Vendor the Shockolate engine and build it via a Docker image with every
build / build (push) Failing after 5s

dependency (SDL2, SDL2_mixer, fluidsynth-lite, a MIDI soundfont)
prebuilt, so compiling the engine needs no network access - just the
image and the engine source. Add res/assets/extract_assets.sh to pull
the game's data files out of a purchased GOG installer, and a Makefile
that assembles a runnable dist/ from the two.

Also add `make package`, which builds a redistributable tarball that
omits the proprietary game assets (shipping res/GET_ASSETS.txt instead)
plus license information for both the MIT tooling and the GPLv3 engine,
and a Gitea Actions workflow that builds and publishes it to
dl.ladkau.de on every push.
This commit is contained in:
ml
2026-07-19 15:43:38 +02:00
parent 3ed6400466
commit a5ec8db5f7
826 changed files with 214554 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
/*
Copyright (C) 1994-1995 Looking Glass Technologies, Inc.
Copyright (C) 2015-2018 Night Dive Studios, LLC.
Copyright (C) 2018-2020 Shockolate Project
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// InitMac.c - Initialize Mac toolbox managers and setup the application's globals.
//--------------------
// Includes
//--------------------
#include <SDL.h>
#include "InitMac.h"
#include "Shock.h"
#include "ShockBitmap.h"
#include "shockolate_version.h"
// Globals
intptr_t *gScreenAddress;
int32_t gScreenRowbytes;
int32_t gScreenWide, gScreenHigh;
// Time Manager routines and globals
uint32_t gShockTicks;
uint32_t *tmd_ticks;
void InitMac(void) {
INFO("Starting %s", SHOCKOLATE_VERSION);
InstallShockTimers(); // needed for the tick pointer
}
void InstallShockTimers(void) {
gShockTicks = 0;
tmd_ticks = &gShockTicks;
}
void CleanupAndExit(void) {
/*Cleanup();
ExitToShell();*/
}
+46
View File
@@ -0,0 +1,46 @@
/*
Copyright (C) 1994-1995 Looking Glass Technologies, Inc.
Copyright (C) 2015-2018 Night Dive Studios, LLC.
Copyright (C) 2018-2020 Shockolate Project
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// InitMac.h - Initialize Mac toolbox managers and setup the application's globals.
// How many tick passed since game startup
extern uint32_t gShockTicks;
// Pointer to screen
extern intptr_t *gScreenAddress;
extern int32_t gScreenRowbytes;
// Size of current window
extern int32_t gScreenWide, gScreenHigh;
//--------------------
// Prototypes
//--------------------
/// Initialize the Macintosh managers.
void InitMac(void);
/**
* Normal cleanup when the program quits.
* @deprecated Actually does nothing
*/
void CleanupAndExit(void);
/// Startup the SystemShock timer.
void InstallShockTimers(void);
+396
View File
@@ -0,0 +1,396 @@
/*
Copyright (C) 2015-2018 Night Dive Studios, LLC.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
//==============================================================================
//
// System Shock - ©1994-1995 Looking Glass Technologies, Inc.
//
// MacTune.c - Rewrite of Shock's MLIMBS.C file to use QuickTime MIDI rather than AIL.
//
//==============================================================================
#include <stdbool.h>
#include "MacTune.h"
#include "miscqvar.h"
#include "Xmi.h"
//-----------------
// GLOBALS
//-----------------
uchar mlimbs_on = FALSE;
char mlimbs_status = 0;
// Handle gHeaderHdl, gTuneHdl, gOfsHdl; // Holds the tune-related data for the current
// theme file.
long *gOffsets; // Array of offsets for the beginning of each tune.
// TunePlayer gPlayer; // The Tune
// Player.
bool gTuneDone; // True when a sequence has finished playing (set by CB proc).
bool gReadyToQueue; // True when it's time to queue up a new sequence.
int gOverlayTime; // Amount of time (in millisecs) to wait for overlays.
int gQueueTime; // Amount of time (in millisecs) to wait to queue next tune.
// TuneCallBackUPP gTuneCBProc; // Pointer to tune-finished callback
// proc.
// CalcTuneTask gCalcTuneTask; // Global to hold task info.
// TimerUPP gCalcTuneProcPtr; // UPP for the 6-second time manager tune
// determiner task.
//-------------------
// INTERNAL PROTOTYPES
//-------------------
// pascal void TuneEndCB(const TuneStatus *status, long refCon);
//---------------------------------------------------------------
// The following section is the time manager task for determining the next tune to play.
//---------------------------------------------------------------
#pragma require_prototypes off
//---------------------------------------------------------------
void CalcTuneProc(void) {
gReadyToQueue = TRUE; // It's time to queue up another tune.
}
#pragma require_prototypes on
//------------------------------------------------------------------------------
// Initializes the MacTune system.
//------------------------------------------------------------------------------
int MacTuneInit(void) {
/*if (mlimbs_status != 0) // If already inited, return
return 0;
//¥¥¥ if (!music_card) return -1;
// Put something here to check for the existence of the QuickTime Musical Instruments. Or maybe
// in music_init.
// See if there is enough memory to load the QuickTime Music stuff. If not, return an error.
Handle syshdl = NewHandleSys(819200); // Try to allocate an 800K handle in system heap.
if (!syshdl) // If it couldn't allocate the
memory, return 1; // return an error. else
DisposeHandle(syshdl); // We don't need this, it was just a
test.
// Load up a player to play the tunes.
gPlayer = OpenDefaultComponent(kTunePlayerType, 0);
if (!gPlayer)
{
DebugString("Error: Could not open a tune player."); //¥¥¥ Handle this!
return 2;
}
// Setup the end-of-tune callback proc.
gTuneCBProc = NewTuneCallBackProc(TuneEndCB);
// Setup a time-manager task for queueing up new tunes.
gCalcTuneProcPtr = NewTimerProc(CalcTuneProc); // Make a UPP for the TM task
gCalcTuneTask.task.tmAddr = gCalcTuneProcPtr; // Insert the calc tune TM task
gCalcTuneTask.task.tmWakeUp = 0;
gCalcTuneTask.task.tmReserved = 0;
#ifndef __powerc
gCalcTuneTask.appA5 = SetCurrentA5();
#endif
InsTime((QElemPtr)&gCalcTuneTask);
// Initialize some globals.
gHeaderHdl = NULL;
gTuneHdl = NULL;
gOfsHdl = NULL;
mlimbs_status = 1;*/
return 0;
}
//------------------------------------------------------------------------------
// Shuts down the system so it stops playing music and releases all resources.
//------------------------------------------------------------------------------
void MacTuneShutdown(void) {
/*if (mlimbs_status == 0) // If already shut down, do
nothing return;
MacTunePurgeCurrentTheme(); // Kill the current theme (unloading theme data)
RmvTime((QElemPtr)&gCalcTuneTask); // Stop the calc tune task
DisposeRoutineDescriptor(gCalcTuneProcPtr); // Dispose its UPP
DisposeRoutineDescriptor(gTuneCBProc); // Cleanup callback and player
CloseComponent(gPlayer);
mlimbs_status = 0;*/
}
//--------------------------------------------------------------------------
// Call-back routine. Get's called when tune is finished.
//--------------------------------------------------------------------------
/*pascal void TuneEndCB(const TuneStatus *s, long refCon)
{
gTuneDone = TRUE;
}*/
//------------------------------------------------------------------------------
// Loads all resources associated with a theme file. Stops and purges and currently loaded theme. Returns
// a 1 if successful, < 0 if an error.
//------------------------------------------------------------------------------
/*int MacTuneLoadTheme(FSSpec *themeSpec, int themeID)
{
short filenum;
Handle binHdl;
Ptr p;
extern uchar track_table[NUM_SCORES][SUPERCHUNKS_PER_SCORE];
extern uchar transition_table[NUM_TRANSITIONS];
extern uchar layering_table[NUM_LAYERS][MAX_KEYS];
extern uchar key_table[NUM_LAYERABLE_SUPERCHUNKS][KEY_BAR_RESOLUTION];
if (mlimbs_status == 0)
// Only do this if MacTune is inited. return (-1);
MacTunePurgeCurrentTheme(); // Purge the current
theme.
// Open the theme file.
filenum = FSpOpenResFile(themeSpec, fsRdPerm);
if (filenum == -1)
return (-2);
// First, get the 'tbin' resource and copy its data to the MusicAI global arrays.
binHdl = GetResource('tbin', 128);
if (binHdl == NULL)
{
CloseResFile(filenum);
Debugger(); //¥¥¥
return (-3);
}
HLock(binHdl);
p = *binHdl;
BlockMoveData(p, track_table, NUM_SCORES * SUPERCHUNKS_PER_SCORE);
p += NUM_SCORES * SUPERCHUNKS_PER_SCORE;
BlockMoveData(p, transition_table, NUM_TRANSITIONS);
p += NUM_TRANSITIONS;
BlockMoveData(p, layering_table, NUM_LAYERS * MAX_KEYS);
p += NUM_LAYERS * MAX_KEYS;
BlockMoveData(p, key_table, NUM_LAYERABLE_SUPERCHUNKS * KEY_BAR_RESOLUTION);
p += NUM_LAYERABLE_SUPERCHUNKS * KEY_BAR_RESOLUTION;
gOverlayTime = *(int *)p;
p += 4;
gQueueTime = *(int *)p;
HUnlock(binHdl);
ReleaseResource(binHdl);
// Next, get the theme-related resources.
gHeaderHdl = GetResource('thdr', 128);
if (gHeaderHdl == NULL)
{
CloseResFile(filenum);
return (-4);
}
gTuneHdl = GetResource('them', 128);
if (gTuneHdl == NULL)
{
CloseResFile(filenum);
return (-5);
}
gOfsHdl = GetResource('tofs', 128);
if (gOfsHdl == NULL)
{
CloseResFile(filenum);
return (-6);
}
DetachResource(gHeaderHdl); // Turn these into normal handles.
HLockHi(gHeaderHdl);
DetachResource(gTuneHdl);
HLockHi(gTuneHdl);
DetachResource(gOfsHdl);
HLockHi(gOfsHdl);
CloseResFile(filenum);
// Set the tune header (load instruments, etc, can take a second or two).
TuneSetHeader(gPlayer, (unsigned long *)*gHeaderHdl);
TunePreroll(gPlayer);
// Setup the tune offset pointer.
gOffsets = (long *)*gOfsHdl;
// Initialize our playtime globals.
gTuneDone = FALSE;
gReadyToQueue = FALSE;
// Here's a big hack. If we're loading theme 0 (machine sounds only), then don't do an
// intro transition.
if (themeID == 0)
current_mode = NORMAL_MODE;
return(1);
}*/
//------------------------------------------------------------------------------
// If there is a theme loaded, start playing it.
//------------------------------------------------------------------------------
void MacTuneStartCurrentTheme(void) {
/*if (mlimbs_status && gTuneHdl) // If MacTune is inited and there is a theme
loaded,
{
int pid = current_request[0].pieceID;
if (pid != 255) // If there is a tune
requested,
{
MacTunePlayTune(pid); // play it right now.
}
else
gTuneDone = TRUE; // else make sure we check again
soon.
}*/
int track = 1 + current_request[0].pieceID;
if (track >= 0 && track < NumTracks) {
int i = 0;
if (!IsPlaying(i)) {
// extern uchar curr_vol_lev;
// int volume = (int)curr_vol_lev * 127 / 100; //convert from 0-100 to 0-127
StartTrack(i, track);
}
}
}
//------------------------------------------------------------------------------
// Stop the current tune playing, stop the tune queue timer task.
//------------------------------------------------------------------------------
/*void MacTuneKillCurrentTheme(void)
{
if (mlimbs_status && gTuneHdl) // Only do this if MacTune is inited and
there is a
{
// current theme. RmvTime((QElemPtr)&gCalcTuneTask); // Remove the TimeManager task that queues up
InsTime((QElemPtr)&gCalcTuneTask); // next tune, and re-insert to prevent any tasks
from
// triggering.
TuneStop(gPlayer, kStopTuneFade); // Stop the current tune (if any).
TuneFlush(gPlayer); // Flush the
queue.
gReadyToQueue = FALSE;
}
}*/
//------------------------------------------------------------------------------
// Stop the current tune playing, stop the tune queue timer task.
//------------------------------------------------------------------------------
void MacTunePurgeCurrentTheme() {
/*MacTuneKillCurrentTheme(); // Kill the current theme.
// Dispose of all the theme's data.
if (gHeaderHdl)
{
HUnlock(gHeaderHdl);
DisposeHandle(gHeaderHdl);
gHeaderHdl = NULL;
}
if (gTuneHdl)
{
HUnlock(gTuneHdl);
DisposeHandle(gTuneHdl);
gTuneHdl = NULL;
}
if (gOfsHdl)
{
HUnlock(gOfsHdl);
DisposeHandle(gOfsHdl);
gOfsHdl = NULL;
}
// Free the tune player component, then open it back up again. This should make the
// game run much faster.
CloseComponent(gPlayer);
gPlayer = OpenDefaultComponent(kTunePlayerType, 0);
if (!gPlayer)
DebugString("Error: Could not open a tune player."); //¥¥¥ Handle this!
// Clear our the current request array.
for (int i = 0; i < MLIMBS_MAX_SEQUENCES -1; i++)
{
current_request[i].pieceID = 255;
}*/
mlimbs_counter = 0;
}
//------------------------------------------------------------------------------
// Play a tune right now (prime the TM task). Usually this is called when music is first started.
//------------------------------------------------------------------------------
void MacTunePlayTune(int tune) {
/*if (tune == 255 || tune == -1)
DebugString("Eep Eep Invalid tune!"); //¥¥¥
if (gOffsets[tune] != -1) // If there really is a tune there, play it
now.
{
TuneQueue(gPlayer, (unsigned long *)(*gTuneHdl + gOffsets[tune]), 0x10000,
0, 0x7FFFFFFF, kTuneStartNow, gTuneCBProc, 0);
PrimeTime((QElemPtr)&gCalcTuneTask, gOverlayTime + gQueueTime);
//¥¥¥ temp
// the above amount for PrimeTime is temporary because we're not doing overlays yet.
// so just queue up the next tune at queue time.
}
// If there was no tune to play this time, set a flag so it will prime the timer again.
else
gTuneDone = TRUE;*/
}
//------------------------------------------------------------------------------
// Add a tune to the tune queue.
//------------------------------------------------------------------------------
void MacTuneQueueTune(int tune) {
/*if (tune == 255 || tune == -1)
DebugString("Eep Eep Invalid tune!"); //¥¥¥
if (gOffsets[tune] != -1) // If there really is a tune there, queue it
up.
{
TuneStatus tpStatus;
TuneGetStatus(gPlayer, &tpStatus); // We'll need this later.
TuneQueue(gPlayer, (unsigned long *)(*gTuneHdl + gOffsets[tune]), 0x10000,
0, 0x7FFFFFFF, 0, gTuneCBProc, 0);
// Normally we don't prime it yet; we want to wait until the currently playing tune finishes.
// However, if tune was playing before we queued this one, go ahead and prime away.
if (tpStatus.queueTime == 0)
PrimeTime((QElemPtr)&gCalcTuneTask, gOverlayTime + gQueueTime);
}
// If there was no tune to queue this time, set a flag so it will prime the timer again.
else
gTuneDone = TRUE;*/
}
//------------------------------------------------------------------------------
// Prime the TM task to trigger the next tune queueing.
//------------------------------------------------------------------------------
void MacTunePrimeTimer(void) {
// PrimeTime((QElemPtr)&gCalcTuneTask, gOverlayTime + gQueueTime);
//¥¥¥ temp
// the above amount for PrimeTime is temporary because we're not doing overlays yet.
// so just queue up the next tune at queue time.
}
void MacTuneUpdateVolume(void) { UpdateVolumeXMI(); }
+70
View File
@@ -0,0 +1,70 @@
/*
Copyright (C) 2015-2018 Night Dive Studios, LLC.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
//==============================================================================
//
// System Shock - ©1994-1995 Looking Glass Technologies, Inc.
//
// MacTune.h - Rewrite of Shock's MLIMBS.H file to use QuickTime MIDI rather than AIL.
//
//==============================================================================
//-----------------
// TYPES & DEFINES
//-----------------
typedef struct {
int pieceID; // Indexes an array of XMIDI_info structs. Specifies which piece to play.
int priority; // Priority of this request.
int loops; // Number of loops. -1 => until deliberately stopped.
uint rel_vol; // Specifies at what relative volume to play it at. (percent)
uint ramp_time; // Specifies the time to ramp in to the specified rel_vol, or ramp out to 0.
int pan; // Note that this pan value affects all channels.
uchar channel_prioritize;
char crossfade; // 0 - don't crossfade, <0 - crossfade out, >0 - crossfade in.
char ramp; // 0 - don't ramp, <0 - ramp out >0 - ramp in
uchar pad;
} mlimbs_request_info;
#define MLIMBS_MAX_SEQUENCES 8
#define MLIMBS_MAX_CHANNELS 8
#include "mlimbs.h"
//-----------------
// EXTERN GLOBALS
//-----------------
extern bool gReadyToQueue; // True when it's time to queue up a new sequence.
// extern TuneCallBackUPP gTuneCBProc; // The tune's callback proc.
// extern CalcTuneTask gCalcTuneTask; // Global to hold task info.
// extern TimerUPP gCalcTuneProcPtr; // UPP for the 6-second time manager
// tune determiner task.
//-----------------
// PROTOTYPES
//-----------------
int MacTuneInit(void);
void MacTuneShutdown(void);
int MacTuneLoadTheme(char *theme, int themeID);
void MacTuneStartCurrentTheme(void);
void MacTuneKillCurrentTheme(void);
void MacTunePurgeCurrentTheme(void);
void MacTunePlayTune(int tune);
void MacTuneQueueTune(int tune);
void MacTunePrimeTimer(void);
void MacTuneUpdateVolume(void);
+147
View File
@@ -0,0 +1,147 @@
/*
Copyright (C) 2018-2020 Shockolate Project
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Modding.h"
#include "lg.h"
#include <stdio.h>
#include <dirent.h>
char *modding_archive_override;
char *modding_additional_files[MAX_MOD_FILES];
int num_mod_files;
int StringEndsWith(char *src, char *dst) {
char *s = strrchr(src, '.');
if (s != NULL)
return (strcmp(s, dst)) == 0;
return 0;
}
int AddResourceFile(char *filename) {
printf("Found resource file: %s\n", filename);
if (num_mod_files < MAX_MOD_FILES) {
char *f = (char *)malloc(strlen(filename) + 1);
strcpy(f, filename);
modding_additional_files[num_mod_files++] = f;
}
return OK;
}
int AddArchiveFile(char *filename) {
printf("Found archive file: %s\n", filename);
char *f = (char *)malloc(strlen(filename) + 1);
strcpy(f, filename);
modding_archive_override = f;
return OK;
}
int ProcessModFile(char *filename, uchar follow_dirs) {
printf("ProcessModFile: %s\n", filename);
if (StringEndsWith(filename, ".dat")) {
AddArchiveFile(filename);
} else if (StringEndsWith(filename, ".res")) {
AddResourceFile(filename);
} else if (follow_dirs) {
ProcessModDirectory(filename);
}
return OK;
}
int ProcessModArgs(int argc, char **argv) {
// Default things
num_mod_files = 0;
int mod_args_start = 1;
// Skip arguments
for (int i = 1; i < argc; i++) {
if (argv[i][0] == '-') {
mod_args_start++;
}
}
// Default the mod list to empty
modding_archive_override = NULL;
for (int i = 0; i < MAX_MOD_FILES; i++) {
modding_additional_files[i] = NULL;
}
// Now go process args
for (int i = mod_args_start; i < argc; i++) {
ProcessModFile(argv[i], TRUE);
}
// shamaz: return value is not checked anyway
return OK;
}
int ProcessModDirectory(char *dirname) {
// Check if this is a directory
printf("ProcessModDirectory %s\n", dirname);
DIR *dp = opendir(dirname);
if (dp != NULL) {
struct dirent *ep;
// Loop through all files here, calling ProcessModFile for each
while ((ep = readdir(dp))) {
printf("ep->d_name %s\n", ep->d_name);
char *buf = (char *)malloc(strlen(dirname) + strlen(ep->d_name) + 2);
strcpy(buf, dirname);
// Windows, why do you have to be weird?
#ifdef _WIN32
strcat(buf, "\\");
#else
strcat(buf, "/");
#endif
strcat(buf, ep->d_name);
ProcessModFile(buf, FALSE);
free(buf);
}
closedir(dp);
}
// shamaz: Returned value is not checked
return OK;
}
int LoadModFiles() {
for (int i = 0; i < num_mod_files; i++) {
if (modding_additional_files[i] != NULL) {
printf("Loading mod file %s\n", modding_additional_files[i]);
ResOpenFile(modding_additional_files[i]);
}
}
// shamaz: Returned value is not checked
return OK;
}
+52
View File
@@ -0,0 +1,52 @@
/*
Copyright (C) 2018-2020 Shockolate Project
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef __MODDING_H
#define __MODDING_H
//--------------------
// Defines
//--------------------
#define MAX_MOD_FILES 100
//--------------------
// Public Globals
//--------------------
// Let people override the default game archive
extern char *modding_archive_override;
// Additional resource files to load
extern char *modding_additional_files[MAX_MOD_FILES];
extern int num_mod_files;
//--------------------
// Function Prototypes
//--------------------
int AddResourceFile(char *filename);
int AddArchiveFile(char *filename);
int ProcessModFile(char *filename, uchar follow_dirs);
int ProcessModArgs(int argc, char **argv);
int ProcessModDirectory(char *dirname);
int LoadModFiles(void);
#endif //__MODDING_H
+978
View File
@@ -0,0 +1,978 @@
#ifdef USE_OPENGL
#include <cstdio>
#include "OpenGL.h"
#ifdef _WIN32
#define GLEW_STATIC 1
#include <SDL.h>
#include <GL/glew.h>
#else
#define GL_GLEXT_PROTOTYPES
#ifdef __APPLE__
#include <OpenGL/gl.h>
#else
#include <GL/gl.h>
#include <GL/glext.h>
#endif
#include <SDL.h>
#include <SDL_opengl.h>
#endif
extern "C" {
#include "mainloop.h"
#include "map.h"
#include "frintern.h"
#include "frflags.h"
#include "player.h"
#include "textmaps.h"
#include "star.h"
#include "tools.h"
#include "Prefs.h"
#include "Shock.h"
#include "faketime.h"
#include "render.h"
#include "wares.h"
extern SDL_Renderer *renderer;
extern SDL_Palette *sdlPalette;
}
#include <map>
#include <sstream>
struct CachedTexture {
SDL_Surface *bitmap;
SDL_Surface *converted;
GLuint texture;
long lastDrawTime;
bool locked;
};
struct Shader {
GLuint shaderProgram;
GLint uniView;
GLint uniProj;
GLint uniNightSight;
GLint uniMutant;
GLint tcAttrib;
GLint lightAttrib;
GLint colorAttrib;
};
struct FrameBuffer {
GLuint frameBuffer;
GLuint stencilBuffer;
GLuint texture;
int width;
int height;
};
#define MAX_CACHED_TEXTURES 1024
static Shader textureShaderProgram;
static Shader colorShaderProgram;
static Shader starShaderProgram;
static FrameBuffer backupBuffer;
static SDL_GLContext context;
static GLuint dynTexture;
static SDL_Palette *opaquePalette;
static SDL_Palette *transparentPalette;
// Texture cache to keep SDL surfaces and GL textures in memory
static std::map<uint64_t, CachedTexture> texturesByBitsPtr;
static float view_scale;
static int phys_width;
static int phys_height;
static int phys_offset_x;
static int phys_offset_y;
static int render_width;
static int render_height;
static bool opengl_enabled = true;
static bool palette_dirty = false;
static bool blend_enabled = true;
static GLuint bound_texture = -1;
// View matrix; Z offset experimentally tweaked for near-perfect alignment
// between GL projection and software projection (sprite screen coordinates)
static const float ViewMatrix[] = {1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, -0.01, 1.0};
// Projection matrix; experimentally tweaked for near-perfect alignment:
// FOV 89.5 deg, aspect ratio 1:1, near plane 0, far plane 100
static const float ProjectionMatrix[] = {1.00876, 0.0, 0.0, 0.0, 0.0, 1.00876, 0.0, 0.0,
0.0, 0.0, -1.0, -1.0, 0.0, 0.0, 0.0, 0.0};
// Identity matrix for sprite rendering
static const float IdentityMatrix[] = {1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0};
static inline GLint get_texture_min_func() {
// convert prefs texture filtering mode to GL min func value
switch (gShockPrefs.doTextureFilter) {
case 0:
return GL_NEAREST;
case 1:
return GL_LINEAR;
// case 2: return GL_LINEAR_MIPMAP_LINEAR;
}
WARN("gShockPrefs.doTextureFilter=%d is invalid; resetting to zero (unflitered)", gShockPrefs.doTextureFilter);
gShockPrefs.doTextureFilter = 0;
return GL_NEAREST;
}
static inline GLint get_texture_mag_func() {
// convert prefs texture filtering mode to GL mag func value
switch (gShockPrefs.doTextureFilter) {
case 0:
return GL_NEAREST;
case 1:
return GL_LINEAR;
case 2:
return GL_LINEAR;
}
WARN("gShockPrefs.doTextureFilter=%d is invalid; resetting to zero (unflitered)", gShockPrefs.doTextureFilter);
gShockPrefs.doTextureFilter = 0;
return GL_NEAREST;
}
static void set_blend_mode(bool enabled) {
// change the blend mode, if not set already
if (blend_enabled != enabled) {
blend_enabled = enabled;
if (blend_enabled) {
glEnable(GL_BLEND);
} else {
glDisable(GL_BLEND);
}
}
}
static void bind_texture(GLuint tex) {
// bind a texture, if not bound already
if (bound_texture != tex) {
bound_texture = tex;
glBindTexture(GL_TEXTURE_2D, bound_texture);
}
}
static GLuint compileShader(GLenum type, const char *source) {
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint status;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status != GL_TRUE) {
char buffer[512];
glGetShaderInfoLog(shader, 512, nullptr, buffer);
ERROR("Error compiling shader: %s", buffer);
return 0;
}
return shader;
}
static GLuint loadShader(GLenum type, const char *filename) {
DEBUG("Loading shader %s", filename);
#ifdef __APPLE__
char fb[1024];
sprintf(fb, "%s/shaders/%s", SDL_GetBasePath(), filename);
#else
char fb[256];
sprintf(fb, "shaders/%s", filename);
#endif
FILE *file = fopen(fb, "r");
if (file == nullptr) {
ERROR("Could not open shader file %s!", fb);
return 0;
}
std::stringstream source;
char line[256];
while (fgets(line, sizeof(line), file)) {
char *c = &line[strlen(line) - 1];
while (c >= line && (*c == '\r' || *c == '\n'))
*(c--) = '\0';
source << line << '\n';
}
fclose(file);
GLuint s = compileShader(type, source.str().c_str());
if (s == 0) {
ERROR("Could not compile shader %s", filename);
}
return s;
}
static int CreateShader(const char *vertexShaderFile, const char *fragmentShaderFile, Shader *outShader) {
GLuint vertShader = loadShader(GL_VERTEX_SHADER, vertexShaderFile);
GLuint fragShader = loadShader(GL_FRAGMENT_SHADER, fragmentShaderFile);
if (vertShader == 0 || fragShader == 0) {
ERROR("Could not create shader %s : %s", vertexShaderFile, fragmentShaderFile);
context = nullptr;
return 1; // Error!
}
GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertShader);
glAttachShader(shaderProgram, fragShader);
glLinkProgram(shaderProgram);
glUseProgram(shaderProgram);
outShader->shaderProgram = shaderProgram;
outShader->uniView = glGetUniformLocation(shaderProgram, "view");
outShader->uniProj = glGetUniformLocation(shaderProgram, "proj");
outShader->uniNightSight = glGetUniformLocation(shaderProgram, "nightsight");
outShader->uniMutant = glGetUniformLocation(shaderProgram, "mutant");
outShader->tcAttrib = glGetAttribLocation(shaderProgram, "texcoords");
outShader->lightAttrib = glGetAttribLocation(shaderProgram, "light");
outShader->colorAttrib = glGetAttribLocation(shaderProgram, "color");
glUniformMatrix4fv(outShader->uniView, 1, false, IdentityMatrix);
glUniformMatrix4fv(outShader->uniProj, 1, false, IdentityMatrix);
return 0;
}
static FrameBuffer CreateFrameBuffer(int width, int height) {
FrameBuffer newBuffer{};
newBuffer.width = width;
newBuffer.height = height;
// Make a frame buffer, texture for color, and render buffer for stencil
glGenFramebuffers(1, &newBuffer.frameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, newBuffer.frameBuffer);
glGenRenderbuffers(1, &newBuffer.stencilBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, newBuffer.stencilBuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_STENCIL_INDEX8, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, newBuffer.stencilBuffer);
glGenTextures(1, &newBuffer.texture);
glBindTexture(GL_TEXTURE_2D, newBuffer.texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, newBuffer.texture, 0);
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
ERROR("Could not make FrameBuffer!: %x \n", status);
context = nullptr;
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
return newBuffer;
}
static void BindFrameBuffer(FrameBuffer *buffer) {
if (buffer == nullptr) {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
} else {
glBindFramebuffer(GL_FRAMEBUFFER, buffer->frameBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, buffer->stencilBuffer);
glViewport(0, 0, buffer->width, buffer->height);
}
}
int init_opengl() {
DEBUG("Initializing OpenGL");
// Are we running in OpenGL mode?
if (SDL_GL_GetCurrentContext() == nullptr) {
ERROR("No OpenGL context! Falling back to Software mode.");
return 1;
}
// Can we create the world rendering context?
SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1);
context = SDL_GL_CreateContext(window);
if (context == nullptr) {
ERROR("Could not create an OpenGL context! Falling back to Software mode.");
return 1;
}
#ifdef _WIN32
glewExperimental = GL_TRUE;
GLenum err = glewInit();
#endif
glEnable(GL_CULL_FACE);
glEnable(GL_BLEND);
glEnable(GL_ALPHA_TEST);
glEnable(GL_POINT_SPRITE);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glAlphaFunc(GL_GEQUAL, 0.05f);
CreateShader("main.vert", "texture.frag", &textureShaderProgram);
CreateShader("main.vert", "color.frag", &colorShaderProgram);
CreateShader("main.vert", "star.frag", &starShaderProgram);
glGenTextures(1, &dynTexture);
// Did the setup go okay?
if (context == nullptr) {
ERROR("OpenGL could not be initialized, falling back to Software mode.");
return 1;
}
int width, height;
SDL_GetWindowSize(window, &width, &height);
opengl_resize(width, height);
// Now make the palettes
opaquePalette = SDL_AllocPalette(256);
transparentPalette = SDL_AllocPalette(256);
return 0;
}
void opengl_resize(int width, int height) {
SDL_GL_MakeCurrent(window, context);
int logical_width, logical_height;
SDL_RenderGetLogicalSize(renderer, &logical_width, &logical_height);
float scale_x = (float)width / logical_width;
float scale_y = (float)height / logical_height;
if (scale_x >= scale_y) {
// physical aspect ratio is wider; black borders left and right
view_scale = scale_y;
} else {
// physical aspect ratio is narrower; black borders at top and bottom
view_scale = scale_x;
}
phys_width = view_scale * logical_width;
phys_height = view_scale * logical_height;
int border_x = width - phys_width;
int border_y = height - phys_height;
phys_offset_x = border_x / 2;
phys_offset_y = border_y / 2;
backupBuffer = CreateFrameBuffer(logical_width, logical_height);
INFO("OpenGL Resize %i %i %i %i", width, height, phys_width, phys_height);
// Redraw the options menu background in the new resolution
extern uchar wrapper_screenmode_hack;
if (wrapper_screenmode_hack) {
render_run();
wrapper_screenmode_hack = false;
}
}
void opengl_change_palette() { palette_dirty = true; }
bool can_use_opengl() { return context != nullptr; }
bool use_opengl() {
return can_use_opengl() && gShockPrefs.doUseOpenGL && opengl_enabled &&
(_current_loop == GAME_LOOP || _current_loop == FULLSCREEN_LOOP) && !global_fullmap->cyber &&
!(_fr_curflags & (FR_PICKUPM_MASK | FR_HACKCAM_MASK));
}
bool should_opengl_swap() {
return can_use_opengl() && gShockPrefs.doUseOpenGL && opengl_enabled &&
(_current_loop == GAME_LOOP || _current_loop == FULLSCREEN_LOOP) && !global_fullmap->cyber;
}
void opengl_end_frame() {
SDL_GL_MakeCurrent(window, context);
// Done rendering to the frame buffer, reset back to normal
BindFrameBuffer(nullptr);
palette_dirty = false;
}
static void updatePalette(SDL_Palette *palette, bool transparent) {
// Update from the base
SDL_SetPaletteColors(palette, sdlPalette->colors, 0, 256);
if (transparent)
palette->colors[0].a = 0x00;
for (int i = 1; i < 256; i++) {
// colors 1..31, except 2: always at maximum light level
// colors 2, 32..255: no minimum brightness, use interpolated vertex light level
// encode emissive property in the top half of the alpha color, since we don't use those bytes
if (i < 32 && i != 2)
palette->colors[i].a = 0xff;
else
palette->colors[i].a = 0x7f;
}
}
static bool nightsight_active() { return WareActive(player_struct.hardwarez_status[HARDWARE_GOGGLE_INFRARED]); }
void opengl_start_frame() {
SDL_GL_MakeCurrent(window, context);
// Start rendering to our frame buffer canvas
BindFrameBuffer(&backupBuffer);
// Setup the render width
int logical_width, logical_height;
SDL_RenderGetLogicalSize(renderer, &logical_width, &logical_height);
render_height = logical_height;
render_width = logical_width;
// Update the palettes for this frame
updatePalette(opaquePalette, false);
updatePalette(transparentPalette, true);
}
void get_hdpi_scaling(int *x_scale, int *y_scale) {
// We may need to scale up our OpenGL output to match some HDPI scaling
int output_width, output_height;
SDL_GetRendererOutputSize(renderer, &output_width, &output_height);
int screen_width, screen_height;
SDL_GetWindowSize(window, &screen_width, &screen_height);
*x_scale = output_width / screen_width;
*y_scale = output_height / screen_height;
}
void opengl_swap_and_restore(SDL_Surface *ui) {
// restore the view backup (without HUD overlay) for incremental
// updates in the subsequent frame
SDL_GL_MakeCurrent(window, context);
glClear(GL_COLOR_BUFFER_BIT);
int x_hdpi_scale, y_hdpi_scale;
get_hdpi_scaling(&x_hdpi_scale, &y_hdpi_scale);
// Set the drawable area for the 3d view
glViewport(phys_offset_x * x_hdpi_scale, phys_offset_y * y_hdpi_scale, phys_width * x_hdpi_scale,
phys_height * y_hdpi_scale);
set_blend_mode(false);
// Bind and setup our general shader program
glUseProgram(textureShaderProgram.shaderProgram);
GLint tcAttrib = textureShaderProgram.tcAttrib;
GLint lightAttrib = textureShaderProgram.lightAttrib;
glUniform1i(textureShaderProgram.uniNightSight, nightsight_active());
glUniformMatrix4fv(textureShaderProgram.uniView, 1, false, IdentityMatrix);
glUniformMatrix4fv(textureShaderProgram.uniProj, 1, false, IdentityMatrix);
// Draw the frame buffer to the screen as a quad
bind_texture(backupBuffer.texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glBegin(GL_TRIANGLE_STRIP);
glVertexAttrib2f(tcAttrib, 1.0f, 0.0f);
glVertexAttrib1f(lightAttrib, 1.0f);
glVertex3f(1.0f, -1.0f, 0.0f);
glVertexAttrib2f(tcAttrib, 1.0f, 1.0f);
glVertexAttrib1f(lightAttrib, 1.0f);
glVertex3f(1.0f, 1.0f, 0.0f);
glVertexAttrib2f(tcAttrib, 0.0f, 0.0f);
glVertexAttrib1f(lightAttrib, 1.0f);
glVertex3f(-1.0f, -1.0f, 0.0f);
glVertexAttrib2f(tcAttrib, 0.0f, 1.0f);
glVertexAttrib1f(lightAttrib, 1.0f);
glVertex3f(-1.0f, 1.0f, 0.0f);
glEnd();
// Finish drawing the 3d view
glFlush();
glUniform1i(textureShaderProgram.uniNightSight, false);
// Check for OpenGL errors that might have happened
GLenum err = glGetError();
if (err != GL_NO_ERROR)
ERROR("OpenGL error: %i", err);
// Blit the UI canvas over the 3d view
SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, ui);
SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND);
SDL_RenderCopy(renderer, texture, NULL, NULL);
SDL_DestroyTexture(texture);
// Finally, swap to the screen
SDL_RenderPresent(renderer);
}
void toggle_opengl() {
if (gShockPrefs.doUseOpenGL) {
switch (gShockPrefs.doTextureFilter) {
case 0: {
message_info("Switching to OpenGL bilinear rendering");
gShockPrefs.doTextureFilter = 1;
} break;
case 1: {
message_info("Switching to sofware rendering");
gShockPrefs.doUseOpenGL = false;
gShockPrefs.doTextureFilter = 0;
} break;
}
} else {
message_info("Switching to OpenGL unfiltered");
gShockPrefs.doUseOpenGL = true;
gShockPrefs.doTextureFilter = 0;
}
SavePrefs();
}
void opengl_set_viewport(int x, int y, int width, int height) {
render_width = width;
render_height = height;
SDL_GL_MakeCurrent(window, context);
int lw, lh;
SDL_RenderGetLogicalSize(renderer, &lw, &lh);
int draw_y = lh - height - y;
glViewport(x, draw_y, width, height);
// Make sure everything starts with a stencil of 0xFF
glEnable(GL_SCISSOR_TEST);
glScissor(x, draw_y, width, height);
glClearStencil(0xFF);
glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
glDisable(GL_SCISSOR_TEST);
// Draw everything with a stencil of 0
opengl_set_stencil(0x00);
}
static bool opengl_cache_texture(CachedTexture toCache, grs_bitmap *bm) {
SDL_GL_MakeCurrent(window, context);
glPixelStorei(GL_UNPACK_ROW_LENGTH, bm->row);
if (texturesByBitsPtr.size() < MAX_CACHED_TEXTURES) {
// We have enough room, generate the new texture
glGenTextures(1, &toCache.texture);
bind_texture(toCache.texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, bm->w, bm->h, 0, GL_RGBA, GL_UNSIGNED_BYTE, toCache.converted->pixels);
texturesByBitsPtr[(uint64_t)bm->bits | ((uint64_t)bm->w << 32u) | ((uint64_t)bm->h << 48u)] = toCache;
return true;
}
// Not enough room, just use the dynTexture
bind_texture(dynTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, bm->w, bm->h, 0, GL_RGBA, GL_UNSIGNED_BYTE, toCache.converted->pixels);
return false;
}
static CachedTexture *opengl_get_texture(grs_bitmap *bm) {
auto iter = texturesByBitsPtr.find((uint64_t)bm->bits | ((uint64_t)bm->w << 32u) | ((uint64_t)bm->h << 48u));
if (iter != texturesByBitsPtr.end()) {
return &iter->second;
}
return nullptr;
}
void opengl_clear_texture_cache() {
if (texturesByBitsPtr.empty())
return;
DEBUG("Clearing OpenGL texture cache.");
SDL_GL_MakeCurrent(window, context);
for (auto iter = texturesByBitsPtr.begin(); iter != texturesByBitsPtr.end();) {
// don't free locked surfaces
if (!iter->second.locked) {
SDL_FreeSurface(iter->second.bitmap);
SDL_FreeSurface(iter->second.converted);
glDeleteTextures(1, &iter->second.texture);
iter = texturesByBitsPtr.erase(iter);
} else {
iter++;
}
}
}
static void setPaletteForBitmap(grs_bitmap *bm, SDL_Surface *surface) {
if (bm->flags & BMF_TRANS)
SDL_SetSurfacePalette(surface, transparentPalette);
else
SDL_SetSurfacePalette(surface, opaquePalette);
}
static void convert_texture(grs_bitmap *bm, bool locked) {
SDL_Surface *surface;
if (bm->type == BMT_RSD8) {
grs_bitmap decoded;
gr_rsd8_convert(bm, &decoded);
surface = SDL_CreateRGBSurfaceFrom(decoded.bits, bm->w, bm->h, 8, bm->row, 0, 0, 0, 0);
} else {
surface = SDL_CreateRGBSurfaceFrom(bm->bits, bm->w, bm->h, 8, bm->row, 0, 0, 0, 0);
}
setPaletteForBitmap(bm, surface);
SDL_Surface *rgba = SDL_ConvertSurfaceFormat(surface, SDL_PIXELFORMAT_RGBA32, 0);
// Cache this new surface.
CachedTexture ct{};
ct.bitmap = surface;
ct.converted = rgba;
ct.lastDrawTime = *tmd_ticks;
ct.locked = locked;
bool cached = opengl_cache_texture(ct, bm);
if (!cached) {
DEBUG("Not enough room to cache texture!");
SDL_FreeSurface(surface);
SDL_FreeSurface(rgba);
}
}
void opengl_cache_wall_texture(int idx, int size, grs_bitmap *bm) {
if (idx < NUM_LOADED_TEXTURES) {
CachedTexture *t = opengl_get_texture(bm);
if (t == nullptr) {
convert_texture(bm, true);
} else {
// Need to refresh this texture
t->lastDrawTime = -1;
palette_dirty = true;
}
}
}
static void set_texture(grs_bitmap *bm) {
CachedTexture *t = opengl_get_texture(bm);
if (t == nullptr) {
// Not cached, have to make it
convert_texture(bm, false);
return;
}
bool isDirty = false;
if (t->locked) {
// Locked surfaces only need to update once
if (palette_dirty && t->lastDrawTime != *tmd_ticks) {
SDL_SetSurfacePalette(t->bitmap, transparentPalette); // Walls should show stars
SDL_BlitSurface(t->bitmap, nullptr, t->converted, nullptr);
isDirty = true;
}
} else {
if (bm->type == BMT_RSD8) {
grs_bitmap decoded;
gr_rsd8_convert(bm, &decoded);
SDL_memmove(t->bitmap->pixels, decoded.bits, bm->w * bm->h);
} else {
SDL_memmove(t->bitmap->pixels, bm->bits, bm->w * bm->h);
}
setPaletteForBitmap(bm, t->bitmap);
SDL_BlitSurface(t->bitmap, nullptr, t->converted, nullptr);
isDirty = true;
}
bind_texture(t->texture);
t->lastDrawTime = *tmd_ticks;
if (isDirty) {
glPixelStorei(GL_UNPACK_ROW_LENGTH, bm->row);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, bm->w, bm->h, 0, GL_RGBA, GL_UNSIGNED_BYTE, t->converted->pixels);
}
}
static void draw_vertex(const g3s_point &vertex, GLint tcAttrib, GLint lightAttrib) {
// Default, per-vertex lighting
float light = 1.0f - (vertex.i / 4096.0f);
if (nightsight_active()) {
light = 1.0f;
} else if (gr_get_fill_type() == FILL_CLUT) {
// Could be a CLUT color instead, use that for lighting
// Ugly hack: We don't get the original light value, so we have to
// recalculate it from the offset into the global lighting lookup
// table.
auto *clut = (uint8_t *)gr_get_fill_parm();
light = 1.0f - (clut - grd_screen->ltab) / 4096.0f;
}
if (tcAttrib >= 0)
glVertexAttrib2f(tcAttrib, vertex.uv.u / 256.0, vertex.uv.v / 256.0);
glVertexAttrib1f(lightAttrib, light);
glVertex3f(vertex.x / 65536.0f, vertex.y / 65536.0f, -vertex.z / 65536.0f);
}
int opengl_draw_tmap(int n, g3s_phandle *vp, grs_bitmap *bm) { return opengl_light_tmap(n, vp, bm); }
int opengl_light_tmap(int n, g3s_phandle *vp, grs_bitmap *bm) {
if (n != 3 && n != 4) {
WARN("Unexpected number of texture vertices (%d)", n);
return CLIP_ALL;
}
SDL_GL_MakeCurrent(window, context);
set_blend_mode(bm->flags & BMF_TRANS);
glUseProgram(textureShaderProgram.shaderProgram);
set_texture(bm);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, get_texture_min_func());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, get_texture_mag_func());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
GLint tcAttrib = textureShaderProgram.tcAttrib;
GLint lightAttrib = textureShaderProgram.lightAttrib;
glUniformMatrix4fv(textureShaderProgram.uniView, 1, false, ViewMatrix);
glUniformMatrix4fv(textureShaderProgram.uniProj, 1, false, ProjectionMatrix);
glBegin(GL_TRIANGLE_STRIP);
draw_vertex(*(vp[1]), tcAttrib, lightAttrib);
draw_vertex(*(vp[0]), tcAttrib, lightAttrib);
draw_vertex(*(vp[2]), tcAttrib, lightAttrib);
if (n > 3)
draw_vertex(*(vp[3]), tcAttrib, lightAttrib);
glEnd();
return CLIP_NONE;
}
static float convx(float x) { return x / 32768.0f / render_width - 1; }
static float convy(float y) { return -y / 32768.0f / render_height + 1; }
int opengl_bitmap(grs_bitmap *bm, int n, grs_vertex **vpl, grs_tmap_info *ti) {
if (n != 4) {
WARN("Unexpected number of bitmap vertices (%d)", n);
return CLIP_ALL;
}
SDL_GL_MakeCurrent(window, context);
set_blend_mode(bm->flags & BMF_TRANS);
glUseProgram(textureShaderProgram.shaderProgram);
GLint tcAttrib = textureShaderProgram.tcAttrib;
GLint lightAttrib = textureShaderProgram.lightAttrib;
glUniformMatrix4fv(textureShaderProgram.uniView, 1, false, IdentityMatrix);
glUniformMatrix4fv(textureShaderProgram.uniProj, 1, false, IdentityMatrix);
set_texture(bm);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, get_texture_min_func());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, get_texture_mag_func());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
float light = 1.0f;
if ((ti->flags & TMF_CLUT) && !nightsight_active()) {
// Ugly hack: We don't get the original 'i' value, so we have to
// recalculate it from the offset into the global lighting lookup
// table.
light = 1.0 - (ti->clut - grd_screen->ltab) / 4096.0f;
}
glUniform1i(textureShaderProgram.uniMutant, (bm->type == BMT_TLUC8) || (bm->flags == BMF_TLUC8));
glBegin(GL_TRIANGLE_STRIP);
glVertexAttrib2f(tcAttrib, 1.0f, 0.0f);
glVertexAttrib1f(lightAttrib, light);
glVertex3f(convx(vpl[1]->x), convy(vpl[1]->y), 0.0f);
glVertexAttrib2f(tcAttrib, 0.0f, 0.0f);
glVertexAttrib1f(lightAttrib, light);
glVertex3f(convx(vpl[0]->x), convy(vpl[0]->y), 0.0f);
glVertexAttrib2f(tcAttrib, 1.0f, 1.0f);
glVertexAttrib1f(lightAttrib, light);
glVertex3f(convx(vpl[2]->x), convy(vpl[2]->y), 0.0f);
glVertexAttrib2f(tcAttrib, 0.0f, 1.0f);
glVertexAttrib1f(lightAttrib, light);
glVertex3f(convx(vpl[3]->x), convy(vpl[3]->y), 0.0f);
glEnd();
glUniform1i(textureShaderProgram.uniMutant, false);
return CLIP_NONE;
}
static void set_color(uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha) {
glVertexAttrib4f(colorShaderProgram.colorAttrib, red / 255.0f, green / 255.0f, blue / 255.0f, alpha / 255.0f);
set_blend_mode(alpha < 255);
}
int opengl_draw_poly(long c, int n_verts, g3s_phandle *p, char gour_flag) {
if (n_verts < 3) {
WARN("Unexpected number of polygon vertices (%d)", n_verts);
return CLIP_ALL;
}
SDL_GL_MakeCurrent(window, context);
glUseProgram(colorShaderProgram.shaderProgram);
glUniformMatrix4fv(colorShaderProgram.uniView, 1, false, ViewMatrix);
glUniformMatrix4fv(colorShaderProgram.uniProj, 1, false, ProjectionMatrix);
if (gour_flag == 1 || gour_flag == 3) {
// translucent; see init_pal_fx() for translucency parameters
switch (c) {
case 247:
set_color(120, 120, 120, 80);
break; // dark fog
case 248:
set_color(170, 170, 170, 80);
break; // medium fog
case 249:
set_color(255, 0, 0, 80);
break; // red fog
case 250:
set_color(0, 255, 0, 80);
break; // green fog
case 251:
set_color(0, 0, 255, 80);
break; // blue fog
case 252:
set_color(240, 240, 240, 80);
break; // light fog
case 253:
set_color(0, 0, 255, 128);
break; // blue force field
case 254:
set_color(0, 255, 0, 128);
break; // green force field
case 255:
set_color(255, 0, 0, 128);
break; // red force field
default:
return CLIP_ALL;
}
} else if (c == 255) {
// transparent
return CLIP_NONE;
} else {
// solid color
SDL_Color color = sdlPalette->colors[c];
set_color(color.r, color.g, color.b, 255);
}
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, get_texture_min_func());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, get_texture_mag_func());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
GLint lightAttrib = colorShaderProgram.lightAttrib;
long a = 0, b = n_verts - 1;
glBegin(GL_TRIANGLE_STRIP);
while (true) {
draw_vertex(*(p[a]), -1, lightAttrib);
if (a++ == b)
break;
draw_vertex(*(p[b]), -1, lightAttrib);
if (b-- == a)
break;
}
glEnd();
return CLIP_NONE;
}
void opengl_set_stencil(int v) {
glEnable(GL_STENCIL_TEST);
glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE);
glStencilFunc(GL_ALWAYS, v, ~0u);
}
void opengl_begin_stars() {
SDL_GL_MakeCurrent(window, context);
glPointSize(1.5 * (render_width / 320.0));
glUseProgram(starShaderProgram.shaderProgram);
glUniformMatrix4fv(starShaderProgram.uniView, 1, false, IdentityMatrix);
glUniformMatrix4fv(starShaderProgram.uniProj, 1, false, IdentityMatrix);
// Only draw stars where the stencil value is 0xFF (Sky!)
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
glStencilFunc(GL_EQUAL, 0xFF, ~0u);
set_blend_mode(true);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, get_texture_mag_func());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, get_texture_mag_func());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glBegin(GL_POINTS);
}
void opengl_end_stars() {
glEnd();
// Turn off the stencil test and blending
opengl_set_stencil(0x00);
set_blend_mode(false);
}
int opengl_draw_star(fix star_x, fix star_y, int c, bool anti_alias) {
SDL_GL_MakeCurrent(window, context);
GLint lightAttrib = starShaderProgram.lightAttrib;
float x = fix_float(star_x);
float y = fix_float(star_y);
// Lower screen resolutions should snap to pixels to avoid shimmering
if (!anti_alias) {
x = (int)x + 0.5f;
y = (int)y + 0.5f;
}
x = (x / render_width) * 2.0 - 1.0;
y = (y / render_height) * -2.0 + 1.0;
// rescale the color so that it's 0..255, 0 = dark, 255 = light
int std_color_base = 208;
int std_color_range = 16;
int color = (std_color_base + std_color_range - 1 - c);
color = (255 * color) / (std_color_range + 1);
glVertexAttrib1f(lightAttrib, color / 255.0f);
glVertex3f(x, y, -0.25f);
return CLIP_NONE;
}
void opengl_begin_sensaround(uchar version) {
if (version == 1) {
// Version 1 of the sensaround is old tech, and should render in software mode :D
opengl_enabled = false;
}
}
void opengl_end_sensaround() { opengl_enabled = true; }
#endif // USE_OPENGL
+73
View File
@@ -0,0 +1,73 @@
#ifndef __MACSRC_OPENGL_H
#define __MACSRC_OPENGL_H
#ifdef __cplusplus
extern "C" {
#endif
#include <3d.h>
#include <SDL.h>
#ifdef USE_OPENGL
int init_opengl();
void opengl_cache_wall_texture(int idx, int size, grs_bitmap *bm);
void opengl_clear_texture_cache();
bool can_use_opengl();
bool use_opengl();
void toggle_opengl();
void opengl_resize(int width, int height);
bool should_opengl_swap();
void opengl_swap_and_restore(SDL_Surface *ui);
void opengl_change_palette();
void opengl_set_viewport(int x, int y, int width, int height);
int opengl_draw_tmap(int n, g3s_phandle *vp, grs_bitmap *bm);
int opengl_light_tmap(int n, g3s_phandle *vp, grs_bitmap *bm);
int opengl_bitmap(grs_bitmap *bm, int n, grs_vertex **vpl, grs_tmap_info *ti);
int opengl_draw_poly(long c, int n_verts, g3s_phandle *p, char gour_flag);
int opengl_draw_star(fix star_x, fix star_y, int c, bool anti_alias);
void opengl_begin_stars();
void opengl_end_stars();
void opengl_set_stencil(int v);
void opengl_start_frame();
void opengl_end_frame();
void opengl_begin_sensaround(uchar version);
void opengl_end_sensaround();
#else
static int init_opengl() { return 0; }
static void opengl_cache_wall_texture(int idx, int size, grs_bitmap *bm) {}
static void opengl_clear_texture_cache(){};
static bool can_use_opengl() { return false; }
static bool use_opengl() { return false; }
static void toggle_opengl() {}
static void opengl_resize(int width, int height) {}
static bool should_opengl_swap() { return false; }
static void opengl_swap_and_restore(SDL_Surface *ui) {}
static void opengl_change_palette() {}
static void opengl_set_viewport(int x, int y, int width, int height) {}
static int opengl_draw_tmap(int n, g3s_phandle *vp, grs_bitmap *bm) { return 0; }
static int opengl_light_tmap(int n, g3s_phandle *vp, grs_bitmap *bm) { return 0; }
static int opengl_bitmap(grs_bitmap *bm, int n, grs_vertex **vpl, grs_tmap_info *ti) { return 0; }
static int opengl_draw_poly(long c, int n_verts, g3s_phandle *p, char gour_flag) { return 0; }
static int opengl_draw_star(fix star_x, fix star_y, int c, bool anti_alias) { return 0; }
static void opengl_begin_stars() {}
static void opengl_end_stars() {}
static void opengl_set_stencil(int v) {}
static void opengl_start_frame() {}
static void opengl_end_frame() {}
static void opengl_begin_sensaround(uchar version) {}
static void opengl_end_sensaround() {}
#endif
#ifdef __cplusplus
}
#endif
#endif
File diff suppressed because it is too large Load Diff
+86
View File
@@ -0,0 +1,86 @@
/*
Copyright (C) 2015-2018 Night Dive Studios, LLC.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
//====================================================================================
//
// System Shock - ©1994-1995 Looking Glass Technologies, Inc.
//
// Prefs.h - Handles saving and loading preferences.
//
//====================================================================================
//--------------------
// Types
//--------------------
typedef struct {
short prefVer; // Version - set to 0 for now.
short prefPlayIntro; // Play intro at startup if non-zero.
// Game Options
short goMsgLength; // 0 - normal, 1 - brief
bool goPopupLabels;
bool goOnScreenHelp;
short goLanguage; // 0 - English, 1 - French, 2 - German
bool goCaptureMouse;
bool goInvertMouseY;
// Sound Options
bool soBackMusic;
bool soSoundFX;
short soMusicVolume;
short soSfxVolume;
short soAudioLogVolume;
short soMidiBackend; // 0 => adlmidi, 1 => native, 2 => fluidsynth
short soMidiOutput; // which of the MIDI backend's outputs to use
// Display Options
short doVideoMode;
short doResolution; // 0 - High, 1 - Low
short doDetail; // 0 - Min, 1-Low, 2-High, 3-Max
short doGamma;
bool doUseQD;
bool doUseOpenGL;
// 0 => unfiltered
// 1 => bilinear
// TODO: add trilinear, anisotropic?
short doTextureFilter;
} ShockPrefs;
//--------------------
// Globals
//--------------------
extern ShockPrefs gShockPrefs;
//--------------------
// Prototypes
//--------------------
void SetDefaultPrefs(void);
int16_t LoadPrefs(void);
int16_t SavePrefs(void);
//-------------------
// Enums
//-------------------
enum OPT_SEQ_ { // Must be in the same order as in wraper.h
OPT_SEQ_ADLMIDI = 0,
OPT_SEQ_NativeMI,
#ifdef USE_FLUIDSYNTH
OPT_SEQ_FluidSyn,
#endif // USE_FLUIDSYNTH
OPT_SEQ_Max
};
+198
View File
@@ -0,0 +1,198 @@
#include "Xmi.h"
#include "MusicDevice.h"
static snd_digi_parms digi_parms_by_channel[SND_MAX_SAMPLES];
#ifdef USE_SDL_MIXER
#include <SDL_mixer.h>
static Mix_Chunk *samples_by_channel[SND_MAX_SAMPLES];
extern SDL_AudioStream *cutscene_audiostream;
extern struct MusicDevice *MusicDev;
extern void AudioStreamCallback(void *userdata, unsigned char *stream, int len);
extern void MusicCallback(void *userdata, Uint8 *stream, int len);
int snd_start_digital(void) {
// Startup the sound system
SDL_AudioSpec spec, obtained;
spec.freq = 48000;
spec.format = AUDIO_S16SYS;
spec.channels = 2;
spec.samples = 2048;
spec.callback = AudioStreamCallback;
spec.userdata = (void *)&cutscene_audiostream;
extern SDL_AudioDeviceID device;
device = SDL_OpenAudioDevice(NULL, 0, &spec, &obtained, 0);
if (device == 0) {
ERROR("Could not open SDL audio: %s", SDL_GetError());
} else {
INFO("Opened Music Stream, deviceID %d, freq %d, size %d, format %d, channels %d, samples %d", device,
obtained.freq, obtained.size, obtained.format, obtained.channels, obtained.samples);
}
if (Mix_Init(MIX_INIT_MP3) < 0) {
ERROR("%s: Init failed", __FUNCTION__);
}
if (Mix_OpenAudio(48000, AUDIO_S16SYS, 2, 2048) < 0) {
ERROR("%s: Couldn't open audio device", __FUNCTION__);
}
Mix_AllocateChannels(SND_MAX_SAMPLES);
Mix_HookMusic(MusicCallback, (void *)&MusicDev);
Mix_VolumeMusic(MIX_MAX_VOLUME); // use max volume for music stream
InitReadXMI();
atexit(Mix_CloseAudio);
atexit(SDL_CloseAudio);
return OK;
}
int snd_sample_play(int snd_ref, int len, uchar *smp, struct snd_digi_parms *dprm) {
// Play one of the VOC format sounds
Mix_Chunk *sample = Mix_LoadWAV_RW(SDL_RWFromConstMem(smp, len), 1);
if (sample == NULL) {
DEBUG("%s: Failed to load sample", __FUNCTION__);
return ERR_NOEFFECT;
}
int loops = dprm->loops > 0 ? dprm->loops - 1 : -1;
int channel = Mix_PlayChannel(-1, sample, loops);
if (channel < 0) {
DEBUG("%s: Failed to play sample", __FUNCTION__);
Mix_FreeChunk(sample);
return ERR_NOEFFECT;
}
if (samples_by_channel[channel])
Mix_FreeChunk(samples_by_channel[channel]);
samples_by_channel[channel] = sample;
digi_parms_by_channel[channel] = *dprm;
snd_sample_reload_parms(&digi_parms_by_channel[channel]);
return channel;
}
void snd_end_sample(int hnd_id) {
Mix_HaltChannel(hnd_id);
if (samples_by_channel[hnd_id]) {
Mix_FreeChunk(samples_by_channel[hnd_id]);
samples_by_channel[hnd_id] = NULL;
}
}
bool snd_sample_playing(int hnd_id) { return Mix_Playing(hnd_id); }
snd_digi_parms *snd_sample_parms(int hnd_id) { return &digi_parms_by_channel[hnd_id]; }
void snd_kill_all_samples(void) {
for (int channel = 0; channel < SND_MAX_SAMPLES; channel++) {
snd_end_sample(channel);
}
// assume we want these too
// StopTheMusic(); // no, don't stop the music
if (cutscene_audiostream != NULL)
SDL_AudioStreamClear(cutscene_audiostream);
}
void snd_sample_reload_parms(snd_digi_parms *sdp) {
// ignore if *sdp is not one of the items in digi_parms_by_channel[]
if (sdp < digi_parms_by_channel || sdp > digi_parms_by_channel + SND_MAX_SAMPLES)
return;
int channel = sdp - digi_parms_by_channel;
if (!Mix_Playing(channel))
return;
// sdp->vol ranges from 0..255
Mix_Volume(channel, (sdp->vol * 128) / 100);
// sdp->pan ranges from 1 (left) to 127 (right)
uint8_t right = 2 * sdp->pan;
Mix_SetPanning(channel, 254 - right, right);
}
int is_playing = 0;
int MacTuneLoadTheme(char *theme_base, int themeID) {
char filename[40];
FILE *f;
int i;
#define NUM_SCORES 8
#define SUPERCHUNKS_PER_SCORE 4
#define NUM_TRANSITIONS 9
#define NUM_LAYERS 32
#define MAX_KEYS 10
#define NUM_LAYERABLE_SUPERCHUNKS 22
#define KEY_BAR_RESOLUTION 2
extern uchar track_table[NUM_SCORES][SUPERCHUNKS_PER_SCORE];
extern uchar transition_table[NUM_TRANSITIONS];
extern uchar layering_table[NUM_LAYERS][MAX_KEYS];
extern uchar key_table[NUM_LAYERABLE_SUPERCHUNKS][KEY_BAR_RESOLUTION];
StopTheMusic();
FreeXMI();
if (strncmp(theme_base, "thm", 3)) {
sprintf(filename, "res/sound/%s/%s.xmi", MusicDev->musicType, theme_base);
ReadXMI(filename);
} else {
sprintf(filename, "res/sound/%s/thm%i.xmi", MusicDev->musicType, themeID);
ReadXMI(filename);
sprintf(filename, "res/sound/thm%i.bin", themeID);
extern FILE *fopen_caseless(const char *path, const char *mode); // see caseless.c
f = fopen_caseless(filename, "rb");
if (f != 0) {
fread(track_table, NUM_SCORES * SUPERCHUNKS_PER_SCORE, 1, f);
fread(transition_table, NUM_TRANSITIONS, 1, f);
fread(layering_table, NUM_LAYERS * MAX_KEYS, 1, f);
fread(key_table, NUM_LAYERABLE_SUPERCHUNKS * KEY_BAR_RESOLUTION, 1, f);
fclose(f);
}
}
return OK;
}
void MacTuneKillCurrentTheme(void) { StopTheMusic(); }
#else
// Sound stubs that do nothing, when SDL Mixer is not found
int snd_start_digital(void) { return OK; }
int snd_sample_play(int snd_ref, int len, uchar *smp, struct snd_digi_parms *dprm) { return OK; }
int snd_alog_play(int snd_ref, int len, uchar *smp, struct snd_digi_parms *dprm) { return OK; }
void snd_end_sample(int hnd_id) {}
void snd_kill_all_samples(void) {}
int MacTuneLoadTheme(char *theme_base, int themeID) { return OK; }
void MacTuneKillCurrentTheme(void) {}
snd_digi_parms *snd_sample_parms(int hnd_id) { return &digi_parms_by_channel[0]; }
bool snd_sample_playing(int hnd_id) { return false; }
void snd_sample_reload_parms(snd_digi_parms *sdp) {}
#endif
// Unimplemented sound stubs
void snd_startup(void) {}
int snd_stop_digital(void) { return 1; }
+321
View File
@@ -0,0 +1,321 @@
/*
Copyright (C) 2015-2018 Night Dive Studios, LLC.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
//====================================================================================
//
// System Shock - ©1994-1995 Looking Glass Technologies, Inc.
//
// Shock.c - Mac-specific initialization and main event loop.
//
//====================================================================================
//--------------------
// Includes
//--------------------
#include <math.h>
#include <SDL.h>
#include "InitMac.h"
#include "Modding.h"
#include "OpenGL.h"
#include "Prefs.h"
#include "Shock.h"
#include "ShockBitmap.h"
#include "amaploop.h"
#include "gr2ss.h"
#include "hkeyfunc.h"
#include "mainloop.h"
#include "setup.h"
#include "shockolate_version.h"
#include "status.h"
#include "version.h"
//--------------------
// Globals
//--------------------
bool gPlayingGame;
grs_screen *cit_screen;
SDL_Window *window;
SDL_Palette *sdlPalette;
SDL_Renderer *renderer;
SDL_AudioDeviceID device;
int num_args;
char **arg_values;
extern grs_screen *svga_screen;
extern frc *svga_render_context;
//--------------------
// Prototypes
//--------------------
extern void init_all(void);
extern void inv_change_fullscreen(uchar on);
extern void object_data_flush(void);
extern errtype load_da_palette(void);
// see Prefs.c
extern void CreateDefaultKeybindsFile(void);
extern void LoadHotkeyKeybinds(void);
extern void LoadMoveKeybinds(void);
//------------------------------------------------------------------------------------
// Main function.
//------------------------------------------------------------------------------------
int main(int argc, char **argv) {
// Save the arguments for later
num_args = argc;
arg_values = argv;
// FIXME externalize this
log_set_quiet(0);
log_set_level(LOG_INFO);
INFO("Logger initialized");
// init mac managers
InitMac();
// Initialize the preferences file.
SetDefaultPrefs();
LoadPrefs();
// see Prefs.c
CreateDefaultKeybindsFile(); // only if it doesn't already exist
// even if keybinds file still doesn't exist, defaults will be set here
LoadHotkeyKeybinds();
LoadMoveKeybinds();
// Process some startup arguments
bool show_splash = !CheckArgument("-nosplash");
// CC: Modding support! This is so exciting.
ProcessModArgs(argc, argv);
// Initialize
init_all();
setup_init();
gPlayingGame = true;
load_da_palette();
gr_clear(0xFF);
// Draw the splash screen
INFO("Showing splash screen");
splash_draw(show_splash);
// Start in the Main Menu loop
_new_mode = _current_loop = SETUP_LOOP;
loopmode_enter(SETUP_LOOP);
// Start the main loop
INFO("Showing main menu, starting game loop");
mainloop(argc, argv);
status_bio_end();
stop_music();
return 0;
}
bool CheckArgument(char *arg) {
if (arg == NULL)
return false;
for (int i = 1; i < num_args; i++) {
if (strcmp(arg_values[i], arg) == 0) {
return true;
}
}
return false;
}
void InitSDL() {
SDL_SetHint(SDL_HINT_NO_SIGNAL_HANDLERS, "1");
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengl");
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_AUDIO) < 0) {
DEBUG("%s: Init failed", __FUNCTION__);
}
// TODO: figure out some universal set of settings that work...
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_BUFFER_SIZE, 32);
gr_init();
extern short svga_mode_data[];
gr_set_mode(svga_mode_data[gShockPrefs.doVideoMode], TRUE);
INFO("Setting up screen and render contexts");
// Create a canvas to draw to
SetupOffscreenBitmaps(grd_cap->w, grd_cap->h);
// Open our window!
char window_title[128];
sprintf(window_title, "System Shock - %s", SHOCKOLATE_VERSION);
window = SDL_CreateWindow(window_title, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, grd_cap->w, grd_cap->h,
SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_OPENGL);
// Create the palette
sdlPalette = SDL_AllocPalette(256);
// Setup the screen
svga_screen = cit_screen = gr_alloc_screen(grd_cap->w, grd_cap->h);
gr_set_screen(svga_screen);
gr_alloc_ipal();
SDL_ShowCursor(SDL_DISABLE);
atexit(SDL_Quit);
SDL_RaiseWindow(window);
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_PRESENTVSYNC);
SDL_RenderSetLogicalSize(renderer, grd_cap->w, grd_cap->h);
// Startup OpenGL
init_opengl();
SDLDraw();
SDL_ShowWindow(window);
}
SDL_Color gamePalette[256];
bool UseCutscenePalette = FALSE; // see cutsloop.c
void SetSDLPalette(int index, int count, uchar *pal) {
static bool gammalut_init = 0;
static uchar gammalut[100 - 10 + 1][256];
if (!gammalut_init) {
double factor = (use_opengl() ? 1.0 : 2.2); // OpenGL uses 2.2
int i, j;
for (i = 10; i <= 100; i++) {
double gamma = (double)i * 1.0 / 100;
gamma = 1 - gamma;
gamma *= gamma;
gamma = 1 - gamma;
gamma = 1 / (gamma * factor);
for (j = 0; j < 256; j++)
gammalut[i - 10][j] = (uchar)(pow((double)j / 255, gamma) * 255);
}
gammalut_init = 1;
INFO("Gamma LUT init\'ed");
}
int gam = gShockPrefs.doGamma;
if (gam < 10)
gam = 10;
if (gam > 100)
gam = 100;
gam -= 10;
for (int i = index; i < index + count; i++) {
gamePalette[i].r = gammalut[gam][*pal++];
gamePalette[i].g = gammalut[gam][*pal++];
gamePalette[i].b = gammalut[gam][*pal++];
gamePalette[i].a = 0xff;
}
if (!UseCutscenePalette) {
// Hack black!
gamePalette[255].r = 0x0;
gamePalette[255].g = 0x0;
gamePalette[255].b = 0x0;
gamePalette[255].a = 0xff;
}
SDL_SetPaletteColors(sdlPalette, gamePalette, 0, 256);
SDL_SetSurfacePalette(drawSurface, sdlPalette);
SDL_SetSurfacePalette(offscreenDrawSurface, sdlPalette);
if (should_opengl_swap())
opengl_change_palette();
}
void SDLDraw() {
if (should_opengl_swap()) {
// We want the UI background to be transparent!
sdlPalette->colors[255].a = 0x00;
// Draw the OpenGL view
opengl_swap_and_restore(drawSurface);
// Set the palette back, and we are done
sdlPalette->colors[255].a = 0xff;
return;
}
// Clear the screen!
SDL_RenderClear(renderer);
SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, drawSurface);
// Blit to the screen by drawing the surface
SDL_Rect srcRect = {0, 0, gScreenWide, gScreenHigh};
SDL_RenderCopy(renderer, texture, &srcRect, NULL);
SDL_DestroyTexture(texture);
// Show everything we've drawn
SDL_RenderPresent(renderer);
}
bool MouseCaptured = FALSE;
extern int mlook_enabled;
void CaptureMouse(bool capture) {
MouseCaptured = (capture && gShockPrefs.goCaptureMouse);
if (!MouseCaptured && mlook_enabled && SDL_GetRelativeMouseMode() == SDL_TRUE) {
SDL_SetRelativeMouseMode(SDL_FALSE);
int w, h;
SDL_GetWindowSize(window, &w, &h);
SDL_WarpMouseInWindow(window, w / 2, h / 2);
} else
SDL_SetRelativeMouseMode(MouseCaptured ? SDL_TRUE : SDL_FALSE);
}
+50
View File
@@ -0,0 +1,50 @@
/*
Copyright (C) 2015-2018 Night Dive Studios, LLC.
Copyright (C) 2018-2020 Shockolate Project
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
//====================================================================================
//
// System Shock - ©1994-1995 Looking Glass Technologies, Inc.
//
// Shock.h - Mac-specific initialization and main event loop.
//
//====================================================================================
#include <SDL.h>
//--------------------
// Function Prototypes
//--------------------
int main(int argc, char **argv);
void InitSDL();
void SetSDLPalette(int index, int count, uchar *pal);
void SDLDraw();
void CaptureMouse(bool capture);
bool CheckArgument(char *name);
//--------------------
// Public Globals
//--------------------
// Is game being playing?
extern bool gPlayingGame;
extern grs_screen *cit_screen;
extern SDL_Renderer *renderer;
extern SDL_Window *window;
+89
View File
@@ -0,0 +1,89 @@
/*
Copyright (C) 1994-1995 Looking Glass Technologies, Inc.
Copyright (C) 2015-2018 Night Dive Studios, LLC.
Copyright (C) 2018-2020 Shockolate Project
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// ShockBitmap.c - Manages off-screen bitmaps and palettes.
//--------------------
// Includes
//--------------------
#include "InitMac.h"
#include "Shock.h"
#include "ShockBitmap.h"
#include "2d.h"
//--------------------
// Globals
//--------------------
SDL_Surface *drawSurface;
SDL_Surface *offscreenDrawSurface;
void ChangeScreenSize(int width, int height) {
if (gScreenWide == width && gScreenHigh == height)
return;
INFO("ChangeScreenSize");
SDL_RenderClear(renderer);
extern bool fullscreenActive;
SDL_SetWindowFullscreen(window, fullscreenActive ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
SDL_SetWindowSize(window, width, height);
SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
SDL_RenderSetLogicalSize(renderer, width, height);
SetupOffscreenBitmaps(width, height);
gScreenWide = width;
gScreenHigh = height;
}
//------------------------------------------------------------------------------------
// Setup the main offscreen bitmaps.
//------------------------------------------------------------------------------------
void SetupOffscreenBitmaps(int width, int height) {
DEBUG("SetupOffscreenBitmaps %i %i", width, height);
if (drawSurface != NULL) {
SDL_FreeSurface(drawSurface);
}
if (offscreenDrawSurface != NULL) {
SDL_FreeSurface(offscreenDrawSurface);
}
drawSurface = SDL_CreateRGBSurface(0, width, height, 8, 0, 0, 0, 0);
if (!drawSurface) {
ERROR("SDL: Failed to create draw surface");
return;
}
offscreenDrawSurface = SDL_CreateRGBSurface(0, width, height, 8, 0, 0, 0, 0);
if (!offscreenDrawSurface) {
ERROR("SDL: Failed to create offscreen draw surface");
return;
}
// Point the renderer at the screen bytes
gScreenRowbytes = drawSurface->w;
gScreenAddress = drawSurface->pixels;
grd_mode_cap.vbase = gScreenAddress;
}
+38
View File
@@ -0,0 +1,38 @@
/*
Copyright (C) 1994-1995 Looking Glass Technologies, Inc.
Copyright (C) 2015-2018 Night Dive Studios, LLC.
Copyright (C) 2018-2020 Shockolate Project
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// ShockBitmap.c - Manages off-screen bitmaps and palettes.
#include <SDL.h>
// Globals
// Current draw surface
extern SDL_Surface *drawSurface;
// Offscreen draw surface
extern SDL_Surface *offscreenDrawSurface;
// Prototypes
/// Change screen size.
void ChangeScreenSize(int width, int height);
/// Setup the main offscreen bitmaps.
void SetupOffscreenBitmaps(int width, int height);
+889
View File
@@ -0,0 +1,889 @@
#include <SDL.h>
#include "Xmi.h"
#include "MusicDevice.h"
#include "Prefs.h"
unsigned int NumTracks;
char ChannelThread[16]; // 16 device channels
int NumUsedChannels; // number of in-use device channels
MIDI_EVENT **TrackEvents;
short *TrackTiming;
unsigned short *TrackUsedChannels;
MIDI_EVENT *ThreadEventList[NUM_THREADS];
int ThreadTiming[NUM_THREADS];
char ThreadChannelRemap[16 * NUM_THREADS];
SDL_atomic_t DeviceChannelVolume[16]; // only msb: 0-127
SDL_atomic_t ThreadPlaying[NUM_THREADS];
SDL_atomic_t ThreadCommand[NUM_THREADS];
MusicDevice *MusicDev;
static SDL_mutex *MyMutex;
void MusicCallback(void *userdata, Uint8 *stream, int len) {
MusicDevice *dev;
SDL_LockMutex(MyMutex);
dev = *(MusicDevice **)userdata;
if (!dev || !dev->isOpen) {
SDL_UnlockMutex(MyMutex);
return;
}
SDL_memset(stream, 0, (size_t)len); // in case we don't get anything
dev->generate(dev, (short *)((void *)stream), len / (int)(2 * sizeof(short)));
SDL_UnlockMutex(MyMutex);
}
void FreeXMI(void) {
unsigned int track;
MIDI_EVENT *event, *next;
for (track = 0; track < NumTracks; track++) {
event = TrackEvents[track];
while (event) {
next = event->next;
if (event->buffer)
free(event->buffer);
free(event);
event = next;
}
}
if (TrackEvents) {
free(TrackEvents);
TrackEvents = 0;
}
if (TrackTiming) {
free(TrackTiming);
TrackTiming = 0;
}
if (TrackUsedChannels) {
free(TrackUsedChannels);
TrackUsedChannels = 0;
}
NumTracks = 0;
}
MIDI_EVENT *NewMIDIEvent(MIDI_EVENT **eventlist, MIDI_EVENT *curevent, int time) {
if (*eventlist == 0) {
*eventlist = curevent = (MIDI_EVENT *)malloc(sizeof(MIDI_EVENT));
curevent->next = 0;
if (time < 0)
curevent->time = 0;
else
curevent->time = time;
curevent->buffer = 0;
curevent->len = 0;
return curevent;
}
if (time < 0) {
MIDI_EVENT *event = (MIDI_EVENT *)malloc(sizeof(MIDI_EVENT));
event->next = *eventlist;
*eventlist = curevent = event;
curevent->time = 0;
curevent->buffer = 0;
curevent->len = 0;
return curevent;
}
if (curevent->time > time)
curevent = *eventlist;
while (curevent->next) {
if (curevent->next->time > time) {
MIDI_EVENT *event = (MIDI_EVENT *)malloc(sizeof(MIDI_EVENT));
event->next = curevent->next;
curevent->next = event;
curevent = event;
curevent->time = time;
curevent->buffer = 0;
curevent->len = 0;
return curevent;
}
curevent = curevent->next;
}
curevent->next = (MIDI_EVENT *)malloc(sizeof(MIDI_EVENT));
curevent = curevent->next;
curevent->next = 0;
curevent->time = time;
curevent->buffer = 0;
curevent->len = 0;
return curevent;
}
int ReadXMI(const char *filename) {
FILE *f;
int size, start, begin, pos, time, end, tempo, tempo_set;
unsigned int i, count, len, chunk_len, quant, status, delta, b0, b1, b2, b3;
unsigned char *data, *p;
short ppqn;
unsigned short used_channels;
MIDI_EVENT *eventlist, *curevent, *prev;
char buf[32];
MusicMode mode = Music_GeneralMidi;
INFO("Reading XMI %s", filename);
extern FILE *fopen_caseless(const char *path, const char *mode); // see caseless.c
f = fopen_caseless(filename, "rb");
if (f == 0) {
ERROR("Could not read XMI");
return 0;
}
fseek(f, 0, SEEK_END);
size = ftell(f);
fseek(f, 0, SEEK_SET);
data = (unsigned char *)malloc(size);
if (fread(data, size, 1, f) != 1) {
free(data);
fclose(f);
return 0;
}
fclose(f);
p = data;
memcpy(buf, p, 4);
p += 4;
if (memcmp(buf, "FORM", 4)) {
free(data);
return 0;
} // is not an xmi
b3 = *p++;
b2 = *p++;
b1 = *p++;
b0 = *p++;
len = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24);
start = p - data;
memcpy(buf, p, 4);
p += 4;
if (!memcmp(buf, "XMID", 4))
NumTracks = 1; // XMI doesn't have XDIR, so there's only one track
else if (memcmp(buf, "XDIR", 4)) {
free(data);
return 0;
} // invalid XMI format
else {
NumTracks = 0;
for (i = 4; i < len; i++) {
memcpy(buf, p, 4);
p += 4;
b3 = *p++;
b2 = *p++;
b1 = *p++;
b0 = *p++;
chunk_len = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24);
i += 8;
if (memcmp(buf, "INFO", 4)) {
p += ((chunk_len + 1) & ~1);
i += ((chunk_len + 1) & ~1);
continue;
}
if (chunk_len < 2)
break;
b0 = *p++;
b1 = *p++;
NumTracks = b0 | (b1 << 8);
break;
}
if (NumTracks == 0) {
free(data);
return 0;
} // xmi must have at least one track
p = data + start + ((len + 1) & ~1);
memcpy(buf, p, 4);
p += 4;
if (memcmp(buf, "CAT ", 4)) {
free(data);
return 0;
} // invalid XMI format
b3 = *p++;
b2 = *p++;
b1 = *p++;
b0 = *p++;
len = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24);
memcpy(buf, p, 4);
p += 4;
if (memcmp(buf, "XMID", 4)) {
free(data);
return 0;
} // invalid XMI format
}
INFO("NumTracks: %i", NumTracks);
TrackEvents = (MIDI_EVENT **)malloc(NumTracks * sizeof(MIDI_EVENT *));
TrackTiming = (short *)malloc(NumTracks * sizeof(short));
TrackUsedChannels = (unsigned short *)malloc(NumTracks * sizeof(unsigned short));
for (i = 0; i < NumTracks; i++)
TrackEvents[i] = 0;
count = 0;
while (p - data < size && count != NumTracks) {
memcpy(buf, p, 4);
p += 4;
b3 = *p++;
b2 = *p++;
b1 = *p++;
b0 = *p++;
len = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24);
if (!memcmp(buf, "FORM", 4)) {
p += 4;
memcpy(buf, p, 4);
p += 4;
b3 = *p++;
b2 = *p++;
b1 = *p++;
b0 = *p++;
len = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24);
}
if (memcmp(buf, "EVNT", 4)) {
p += ((len + 1) & ~1);
continue;
}
eventlist = 0;
curevent = 0;
time = 0;
end = 0;
tempo = 500000;
tempo_set = 0;
status = 0;
used_channels = 0;
begin = p - data;
while (!end && p - data < size) {
quant = 0;
for (i = 0; i < 4; i++) {
b0 = *p++;
if (b0 & 0x80) {
p--;
break;
}
quant += b0;
}
time += quant * 3;
status = *p++;
switch (status >> 4) {
case 0x9: // note on
used_channels |= (1 << (status & 15));
b0 = *p++;
curevent = NewMIDIEvent(&eventlist, curevent, time);
curevent->status = status;
curevent->data[0] = b0;
curevent->data[1] = *p++;
delta = 0;
for (i = 0; i < 4; i++) {
b1 = *p++;
delta <<= 7;
delta |= b1 & 0x7F;
if (!(b1 & 0x80))
break;
}
prev = curevent;
curevent = NewMIDIEvent(&eventlist, curevent, time + delta * 3);
curevent->status = status;
curevent->data[0] = b0;
curevent->data[1] = 0;
curevent = prev;
break;
case 0x8:
case 0xA:
case 0xB:
case 0xE: // note off, aftertouch, controller, pitch wheel
used_channels |= (1 << (status & 15));
curevent = NewMIDIEvent(&eventlist, curevent, time);
curevent->status = status;
curevent->data[0] = *p++;
curevent->data[1] = *p++;
break;
case 0xC:
case 0xD: // program change, pressure
used_channels |= (1 << (status & 15));
curevent = NewMIDIEvent(&eventlist, curevent, time);
curevent->status = status;
curevent->data[0] = *p++;
break;
case 0xF: // sysex
if (status == 0xFF) {
pos = p - data;
b0 = *p++;
if (b0 == 0x2F)
end = 1;
else if (b0 == 0x51 && !tempo_set) {
p++;
b3 = *p++;
b2 = *p++;
b1 = *p++;
tempo = (b1 | (b2 << 8) | (b3 << 16)) * 3;
tempo_set = 1;
} else if (b0 == 0x51 && tempo_set) {
quant = 0;
for (i = 0; i < 4; i++) {
b1 = *p++;
quant <<= 7;
quant |= b1 & 0x7F;
if (!(b1 & 0x80))
break;
}
p += quant;
break;
}
p = data + pos;
}
curevent = NewMIDIEvent(&eventlist, curevent, time);
curevent->status = status;
if (status == 0xFF)
curevent->data[0] = *p++;
quant = 0;
for (i = 0; i < 4; i++) {
b0 = *p++;
quant <<= 7;
quant |= b0 & 0x7F;
if (!(b0 & 0x80))
break;
}
curevent->len = quant;
if (curevent->len) {
curevent->buffer = (unsigned char *)malloc(curevent->len);
memcpy(curevent->buffer, p, curevent->len);
p += curevent->len;
}
break;
default:
break;
}
}
ppqn = (tempo * 3) / 25000;
if (!ppqn)
break; // unable to convert data
TrackEvents[count] = eventlist;
TrackTiming[count] = ppqn;
TrackUsedChannels[count] = used_channels;
count++;
p = data + begin + ((len + 1) & ~1);
}
if (count != NumTracks) // failed to extract all tracks from XMI
{
free(data);
NumTracks = count;
FreeXMI();
return 0;
}
free(data);
// Setup a sound bank for res/sound/sblaster, or res/sound/genmidi
if (strstr(filename, "sblaster") != NULL)
mode = Music_SoundBlaster;
SDL_LockMutex(MyMutex);
if (MusicDev) {
MusicDev->setupMode(MusicDev, mode);
}
SDL_UnlockMutex(MyMutex);
switch (mode) {
case Music_GeneralMidi:
INFO("Set General MIDI mode");
break;
case Music_SoundBlaster:
INFO("Set Sound Blaster mode");
break;
}
return 1; // success
}
int MyThread(void *arg) {
int i;
MIDI_EVENT *event[NUM_THREADS];
int ppqn[NUM_THREADS];
double Ippqn[NUM_THREADS];
int tempo[NUM_THREADS];
double tick[NUM_THREADS];
double last_tick[NUM_THREADS];
double last_time[NUM_THREADS];
unsigned int start[NUM_THREADS];
for (i = 0; i < NUM_THREADS; i++) {
event[i] = 0;
ppqn[i] = 1;
Ippqn[i] = 1;
tempo[i] = 0x07A120;
tick[i] = 1;
last_tick[i] = 0;
last_time[i] = 0;
start[i] = 0;
SDL_AtomicSet(&ThreadPlaying[i], 0);
SDL_AtomicSet(&ThreadCommand[i], THREAD_READY);
}
for (;;) {
int delay = 1;
for (i = 0; i < NUM_THREADS; i++) {
if (event[i] && SDL_AtomicGet(&ThreadCommand[i]) != THREAD_STOPTRACK) {
double aim = last_time[i] + (event[i]->time - last_tick[i]) * tick[i];
double diff = aim - ((SDL_GetTicks() - start[i]) * 1000.0);
if (diff > 0) {
if (diff < 1200.0)
delay = 0;
continue;
}
delay = 0;
last_tick[i] = event[i]->time;
last_time[i] = aim;
if (event[i]->status == 0xFF && event[i]->data[0] == 0x51) // tempo change
{
tempo[i] = (event[i]->buffer[0] << 16) | (event[i]->buffer[1] << 8) | event[i]->buffer[2];
tick[i] = tempo[i] * Ippqn[i];
} else if (event[i]->status >= 0x80 && event[i]->status < 0xF0) {
int channel = (event[i]->status & 15);
if (channel != 9)
channel = ThreadChannelRemap[channel + 16 * i]; // remap channel, except 9 (percussion)
uint8_t p1 = event[i]->data[0];
uint8_t p2 = event[i]->data[1];
if ((event[i]->status & ~15) == 0xB0 && event[i]->data[0] == 0x07) {
// set volume msb
// store in array in case global volume changes later
SDL_AtomicSet(&DeviceChannelVolume[channel], p2); // 0-127
// send volume change to device
SDL_LockMutex(MyMutex);
if (MusicDev && MusicDev->isOpen) {
// scale new volume according to global music volume
extern uchar curr_vol_lev; // 0-100
const int scaledVolume = ((int)p2 * (int)curr_vol_lev) / 100;
MusicDev->sendControllerChange(MusicDev, channel, p1, scaledVolume);
}
SDL_UnlockMutex(MyMutex);
} else {
SDL_LockMutex(MyMutex);
if (MusicDev && MusicDev->isOpen) {
switch (event[i]->status & ~15) {
case 0x80:
MusicDev->sendNoteOff(MusicDev, channel, p1, p2);
break;
case 0x90:
MusicDev->sendNoteOn(MusicDev, channel, p1, p2);
break;
case 0xA0:
MusicDev->sendNoteAfterTouch(MusicDev, channel, p1, p2);
break;
case 0xB0:
MusicDev->sendControllerChange(MusicDev, channel, p1, p2);
break;
case 0xC0:
MusicDev->sendProgramChange(MusicDev, channel, p1);
break;
case 0xD0:
MusicDev->sendChannelAfterTouch(MusicDev, channel, p1);
break;
case 0xE0:
MusicDev->sendPitchBendML(MusicDev, channel, p2, p1);
break;
}
}
SDL_UnlockMutex(MyMutex);
}
}
event[i] = event[i]->next;
if (event[i] == 0)
SDL_AtomicSet(&ThreadCommand[i], THREAD_STOPTRACK);
}
if (SDL_AtomicGet(&ThreadCommand[i]) == THREAD_STOPTRACK) {
int channel;
delay = 0;
event[i] = 0;
last_tick[i] = 0;
last_time[i] = 0;
start[i] = SDL_GetTicks();
// here we should turn off all notes for these channels plus 9 (percussion)
for (channel = 0; channel < 16; channel++)
if (ChannelThread[channel] == i) {
ChannelThread[channel] = -1;
NumUsedChannels--;
SDL_AtomicSet(&DeviceChannelVolume[channel], 0);
}
SDL_AtomicSet(&ThreadPlaying[i], 0);
SDL_AtomicSet(&ThreadCommand[i], THREAD_READY);
}
if (SDL_AtomicGet(&ThreadCommand[i]) == THREAD_PLAYTRACK) {
delay = 0;
event[i] = ThreadEventList[i];
ppqn[i] = ThreadTiming[i];
Ippqn[i] = 1.0 / ppqn[i];
tempo[i] = 0x07A120;
tick[i] = tempo[i] * Ippqn[i];
last_tick[i] = 0;
last_time[i] = 0;
start[i] = SDL_GetTicks();
SDL_AtomicSet(&ThreadPlaying[i], 1);
SDL_AtomicSet(&ThreadCommand[i], THREAD_READY);
}
if (SDL_AtomicGet(&ThreadCommand[i]) == THREAD_EXIT)
return 0;
}
SDL_Delay(delay);
}
return 0;
}
int GetTrackNumChannels(unsigned int track) {
int num = 0, channel;
// count channels used by track (could be zero if only percussion channel (9) is used)
for (channel = 0; channel < 16; channel++)
if (channel != 9 && (TrackUsedChannels[track] & (1 << channel)))
num++;
return num;
}
void StartTrack(int thread, unsigned int track) {
int num, trackChannel, deviceChannel;
char channel_remap[16];
if (track >= NumTracks)
return;
num = GetTrackNumChannels(track);
while (SDL_AtomicGet(&ThreadCommand[thread]) != THREAD_READY)
SDL_Delay(1);
// check if enough device channels free; 16 channels available except one (percussion)
if (NumUsedChannels + num <= 16 - 1) {
NumUsedChannels += num;
memset(channel_remap, 0, 16);
// assign channels used by track to device channels that are currently free
for (trackChannel = 0; trackChannel < 16; trackChannel++) {
// only map used, non-percussion channels
if (trackChannel != 9 && (TrackUsedChannels[track] & (1 << trackChannel))) {
// find first unassigned device channel
for (deviceChannel = 0; deviceChannel < 16; deviceChannel++) {
if (deviceChannel != 9 && ChannelThread[deviceChannel] == -1)
break;
}
channel_remap[trackChannel] = deviceChannel;
ChannelThread[deviceChannel] = thread;
// default to full volume
SDL_AtomicSet(&DeviceChannelVolume[deviceChannel], 127);
}
}
ThreadEventList[thread] = TrackEvents[track];
ThreadTiming[thread] = TrackTiming[track];
memcpy(ThreadChannelRemap + 16 * thread, channel_remap, 16);
SDL_AtomicSet(&ThreadCommand[thread], THREAD_PLAYTRACK);
while (SDL_AtomicGet(&ThreadCommand[thread]) != THREAD_READY)
SDL_Delay(1);
}
}
void StopTrack(int i) {
if (!SDL_AtomicGet(&ThreadPlaying[i]))
return;
while (SDL_AtomicGet(&ThreadCommand[i]) != THREAD_READY)
SDL_Delay(1);
SDL_AtomicSet(&ThreadCommand[i], THREAD_STOPTRACK);
while (SDL_AtomicGet(&ThreadCommand[i]) != THREAD_READY)
SDL_Delay(1);
}
void StopTheMusic(void) {
int i;
for (i = 0; i < NUM_THREADS; i++)
StopTrack(i);
SDL_LockMutex(MyMutex);
if (MusicDev) {
MusicDev->reset(MusicDev);
}
SDL_UnlockMutex(MyMutex);
}
int IsPlaying(int i) { return SDL_AtomicGet(&ThreadPlaying[i]); }
void InitReadXMI(void) {
int channel, i;
SDL_Thread *thread;
InitDecXMI();
MyMutex = SDL_CreateMutex();
for (channel = 0; channel < 16; channel++) {
ChannelThread[channel] = -1;
SDL_AtomicSet(&DeviceChannelVolume[channel], 0);
}
for (i = 0; i < NUM_THREADS; i++) {
SDL_AtomicSet(&ThreadPlaying[i], 0);
SDL_AtomicSet(&ThreadCommand[i], THREAD_INIT);
}
thread = SDL_CreateThread(MyThread, "MyThread", NULL);
SDL_DetachThread(thread); // thread will go away on its own upon completion
i = 0;
while (SDL_AtomicGet(&ThreadCommand[i]) == THREAD_INIT)
SDL_Delay(1);
atexit(ShutdownReadXMI);
}
void InitDecXMI(void) {
SDL_LockMutex(MyMutex);
if (MusicDev) {
WARN("InitDecXMI(): *****WARNING***** Creating new music device, but one already exists!");
}
// Start the Midi device
MusicDevice *musicdev = NULL;
int musicrate = 48000;
switch (gShockPrefs.soMidiBackend) {
case OPT_SEQ_ADLMIDI: // adlmidi
{
INFO("Creating ADLMIDI device");
musicdev = CreateMusicDevice(Music_AdlMidi);
} break;
case OPT_SEQ_NativeMI: // native midi
{
INFO("Creating native MIDI device");
musicdev = CreateMusicDevice(Music_Native);
} break;
#ifdef USE_FLUIDSYNTH
case OPT_SEQ_FluidSyn: // fluidsynth
{
INFO("Creating FluidSynth MIDI device");
musicdev = CreateMusicDevice(Music_FluidSynth);
} break;
#endif
}
// init chosen music device
INFO("Opening MIDI device using output %d", gShockPrefs.soMidiOutput);
if (musicdev && musicdev->init(musicdev, gShockPrefs.soMidiOutput, musicrate) != 0) {
musicdev->destroy(musicdev);
musicdev = NULL;
}
// fallback to dummy
if (!musicdev) {
WARN("Using dummy MIDI driver");
musicdev = CreateMusicDevice(Music_None);
if (musicdev) {
musicdev->init(musicdev, gShockPrefs.soMidiOutput, musicrate);
}
}
// force prefs to align with music device output
if (musicdev) {
gShockPrefs.soMidiOutput = musicdev->outputIndex;
}
MusicDev = musicdev;
SDL_UnlockMutex(MyMutex);
}
void ReloadDecXMI(void) {
int i;
// determine whether a device type change is being requested, by comparing the
// current device type (if any) with current preferences setting
short deviceTypeMatch = 0;
SDL_LockMutex(MyMutex);
if (MusicDev) {
switch (MusicDev->deviceType) {
case Music_None:
deviceTypeMatch = 0;
break;
case Music_AdlMidi:
deviceTypeMatch = (gShockPrefs.soMidiBackend == 0);
break;
case Music_Native:
deviceTypeMatch = (gShockPrefs.soMidiBackend == 1);
break;
#ifdef USE_FLUIDSYNTH
case Music_FluidSynth:
deviceTypeMatch = (gShockPrefs.soMidiBackend == 2);
break;
#endif
}
}
// only destroy the device if it exists and any of the following apply:
// - it hasn't been opened yet
// - device type change requested
// - device output change requested
// this is needed to protect against reload spam generated by the UI slider
if (MusicDev && (!MusicDev->isOpen || !deviceTypeMatch || MusicDev->outputIndex != gShockPrefs.soMidiOutput)) {
SDL_UnlockMutex(MyMutex);
INFO("Closing MIDI driver due to reload");
for (i = 0; i < NUM_THREADS; i++) {
StopTrack(i);
}
SDL_LockMutex(MyMutex);
MusicDev->destroy(MusicDev);
MusicDev = NULL;
}
// only init music device if it doesn't still exist
if (!MusicDev) {
SDL_UnlockMutex(MyMutex);
InitDecXMI();
} else {
SDL_UnlockMutex(MyMutex);
}
}
void ShutdownReadXMI(void) {
int i;
for (i = 0; i < NUM_THREADS; i++) {
StopTrack(i);
// don't set THREAD_EXIT yet, as this seems to cause deadlocks
}
SDL_Delay(50);
for (i = 0; i < NUM_THREADS; i++) {
SDL_AtomicSet(&ThreadCommand[i], THREAD_EXIT);
}
SDL_Delay(50); // wait a bit for thread to hopefully exit
INFO("Closing MIDI driver due to shutdown");
SDL_LockMutex(MyMutex);
if (MusicDev) {
MusicDev->destroy(MusicDev);
MusicDev = NULL;
} else {
WARN("ShutdownReadXMI(): Shutdown request received, but no music device exists!");
}
SDL_UnlockMutex(MyMutex);
FreeXMI();
SDL_DestroyMutex(MyMutex);
}
unsigned int GetOutputCountXMI(void) {
unsigned int outputCount = 0;
SDL_LockMutex(MyMutex);
if (MusicDev) {
outputCount = MusicDev->getOutputCount(MusicDev);
}
SDL_UnlockMutex(MyMutex);
return outputCount;
}
void GetOutputNameXMI(const unsigned int outputIndex, char *buffer, const unsigned int bufferSize) {
SDL_LockMutex(MyMutex);
if (MusicDev && buffer && bufferSize >= 1) {
MusicDev->getOutputName(MusicDev, outputIndex, buffer, bufferSize);
}
SDL_UnlockMutex(MyMutex);
}
void UpdateVolumeXMI(void) {
// global volume has been changed
extern uchar curr_vol_lev; // 0-100
INFO("UpdateVolumeXMI(): Global music volume change to %d percent", curr_vol_lev);
// tell the music driver
SDL_LockMutex(MyMutex);
if (MusicDev && MusicDev->isOpen) {
// send volume change controller (#7) for all channels
for (int i = 0; i <= 15; ++i) {
// skip unused device channels
if (i != 9 && ChannelThread[i] == -1)
continue;
// scale new volume according to global music volume
const int scaledVolume = ((int)SDL_AtomicGet(&DeviceChannelVolume[i]) * (int)curr_vol_lev) / 100;
MusicDev->sendControllerChange(MusicDev, i, 7, scaledVolume);
}
}
SDL_UnlockMutex(MyMutex);
}
+54
View File
@@ -0,0 +1,54 @@
#define NUM_THREADS 8
#define THREAD_INIT 0
#define THREAD_READY 1
#define THREAD_PLAYTRACK 2
#define THREAD_STOPTRACK 3
#define THREAD_EXIT 4
extern unsigned int NumTracks;
//-1: no thread is using this device channel; 0- : thread index that is using this device channel
extern char ChannelThread[16]; // 16 device channels
extern int NumUsedChannels; // number of in-use device channels
void FreeXMI(void);
int ReadXMI(const char *filename);
void StartTrack(int i, unsigned int track);
void StopTrack(int i);
void StopTheMusic(void);
int IsPlaying(int i);
void InitReadXMI(void);
void InitDecXMI(void);
void ReloadDecXMI(void);
void ShutdownReadXMI(void);
unsigned int GetOutputCountXMI(void);
void GetOutputNameXMI(const unsigned int outputIndex, char *buffer, const unsigned int bufferSize);
void UpdateVolumeXMI(void);
struct midi_event_struct {
int time;
unsigned char status;
unsigned char data[2];
unsigned int len;
unsigned char *buffer;
struct midi_event_struct *next;
};
typedef struct midi_event_struct MIDI_EVENT;
extern MIDI_EVENT **TrackEvents;
extern short *TrackTiming;
extern unsigned short *TrackUsedChannels;
extern MIDI_EVENT *ThreadEventList[NUM_THREADS];
extern int ThreadTiming[NUM_THREADS];
extern char ThreadChannelRemap[16 * NUM_THREADS];
extern SDL_atomic_t DeviceChannelVolume[16]; // only msb: 0-127
extern SDL_atomic_t ThreadPlaying[NUM_THREADS];
extern SDL_atomic_t ThreadCommand[NUM_THREADS];
struct thread_data {
int i; // thread index
};