/* * * Singe 3 * Copyright (C) 2006-2026 Scott Duensing * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. * */ #include #include #include #include #include #include #include #ifdef _WIN32 // _describeCpu reads the processor name out of the registry, which needs the Windows headers. #include #include #else #include #endif #include #ifndef _WIN32 #include #include #endif #ifdef __APPLE__ #include #endif #include #include #include #include #include #include "../thirdparty/arg_parser/carg_parser.h" #include "../thirdparty/uthash/src/utlist.h" #include "main.h" #include "midi.h" #include "midiIo.h" #include "stddclmr.h" #include "util.h" #include "frameFile.h" #include "videoPlayer.h" #include "singe.h" #include "pack.h" #include "vfs.h" #include "render.h" #include "../thirdparty/ffmpeg/libavformat/avformat.h" #include "embedded.h" #define MENU_OPTIONS "-k -w -v" #define MENU_BINARY_PREFIX "Singe" // The launcher searches for this beside itself; the binary is named Singe-vX.YY-... #define PRIMARY_DISPLAY 0 #define MIXER_FREQUENCY 44100 #define MIXER_CHANNELS 2 #define MIXER_CHUNK_SAMPLES "1024" // Device buffer, kept small so the audio queue and any error in measuring it stay small #define USAGE_OPTION_WIDTH 28 // The longest long form is --trigger_threshold=PERCENT plus a space #define USAGE_LINE_WIDTH 79 // Help text wraps so no line is wider than this #define CRASH_FRAMES_MAX 64 #define SCREEN_MIN 1 // --screen numbers the displays from 1, as Hypseus does #define IDLE_EXIT_MIN 0 // --idleexit seconds; 0 never quits #define IDLE_EXIT_MAX 86400 // ... and a day is as long as anyone can mean #define RATIO_MIN 0.0 // --xratio and --yratio; 0 is "not given", Hypseus's own default #define RATIO_MAX 100.0 #define RATIO_PRECISION 100.0 // Truncated to two decimals, as Hypseus truncates them #define FVALUE_MIN 0.0 // --fvalue #define FVALUE_MAX 100000.0 #define FVALUE_PRECISION 1000.0 // ... and three decimals for this one #define API_VERSION_PROTOCOL 1 // Format version of the --apiversion line; see the manual #define TRACE_HEADER_RULE "----------------------------------------------------------------" typedef struct RatioS { int32_t aspectNum; int32_t aspectDom; } RatioT; typedef struct ResolutionS { int32_t width; int32_t height; } ResolutionT; typedef struct ModeS { RatioT ratio; ResolutionT resolution; } ModeT; typedef struct QueueS { ConfigT *conf; struct QueueS *next; } QueueT; // Options with no short letter: a code outside the unsigned char range is how carg_parser names // one. The alphabet ran out, and a letter picked at random helps nobody. typedef enum LongOptionE { OPT_ABSOLUTES_ONLY = 256, OPT_ALTAUDIO, OPT_APIVERSION, OPT_DEINTERLACE, OPT_DETERMINISTIC, OPT_FVALUE, OPT_GAMEPAD_REORDER, OPT_HAPTIC, OPT_IDLEEXIT, OPT_JOYMOUSE, OPT_JS_RANGE, OPT_KEYMAPFILE, OPT_LINEARSCALE, OPT_MANYMOUSE, OPT_MAPJOYSTICKS, OPT_MONOCHROME, OPT_NOGAMEPAD, OPT_SCREEN, OPT_SOUNDFONT, OPT_STARTSILENT, OPT_TRIGGER_THRESHOLD, OPT_XRATIO, OPT_YRATIO } LongOptionE; typedef struct OptionS { int32_t code; const char *name; enum ap_Has_arg hasArgument; const char *value; // Placeholder shown in the usage text, NULL when the option takes none. const char *help; bool hidden; // Parsed but not listed in the usage text. bool settable; // May be set in settings.cfg; the ones naming the game or a path may not. } OptionT; typedef struct EmbeddedFileS { const char *name; const uint8_t *data; size_t length; } EmbeddedFileT; static QueueT *_scriptQueue = NULL; // Single source of truth for the command line: feeds both the parser and the usage text. static const OptionT _options[] = { { 'a', "aspect", ap_yes, "N:D", "force aspect ratio", false, true }, { 'A', "audiodelay", ap_yes, "MS", "compensate for audio heard MS milliseconds late (negative if early)", false, true }, { 'B', "bezel", ap_yes, "FILENAME", "cabinet artwork from the bezels folder, drawn around the picture", false, true }, { 'b', "scalefactor", ap_yes, "PERCENT", "reduce screen size for overscan compensation", false, true }, { 'c', "showcalculated", ap_no, NULL, "show calculated framefile values for debugging", false, true }, { 'C', "canvas", ap_yes, "WxH", "world size for games without a disc (default 720x480)", false, true }, { 'D', "disc", ap_no, NULL, "play a laserdisc video (implied by --framefile)", false, false }, { 'd', "datadir", ap_yes, "PATHNAME", "alternate location for written files", false, false }, { 'E', "entry", ap_yes, "N", "run the Nth games.dat entry of a .game file (default 1)", false, false }, { 'e', "volume_nonvldp", ap_yes, "PERCENT", "specify sound effects volume in percent", false, true }, { 'F', "bezelflip", ap_no, NULL, "draw the bezel artwork in front of the picture instead of behind it", false, true }, { 'f', "fullscreen", ap_no, NULL, "run in full screen mode", false, true }, { 'g', "sindengun", ap_yes, "'PARAMS'", "enable Sinden Light Gun support", false, true }, { 'G', "gamedir", ap_yes, "PATHNAME", "directory holding the games and the Singe folder (default: the current one)", false, false }, { 'H', "softwarevideo", ap_no, NULL, "decode video in software even when a hardware decoder exists", false, true }, { 'h', "help", ap_no, NULL, "this display", false, false }, { 'I', "bezeldir", ap_yes, "PATHNAME", "folder holding the bezel artwork, in place of bezels", false, true }, { 'k', "nologos", ap_no, NULL, "kill the splash screens", false, true }, { 'l', "volume_vldp", ap_yes, "PERCENT", "specify laserdisc volume in percent", false, true }, { 'm', "nomouse", ap_no, NULL, "disable mouse", false, true }, { 'n', "nocrosshair", ap_no, NULL, "request game not display gun crosshairs", false, true }, { 'o', "audio", ap_yes, "TRACK", "select default track for audio output", false, true }, { 'P', "pack", ap_yes, "DIRECTORY", "pack the game in DIRECTORY into the .game named after the options, or changed files into a .patch", false, false }, { 'p', "program", ap_no, NULL, "trace Singe execution to screen and file", false, true }, { 'R', "reload", ap_no, NULL, "reload the game when a loose script file changes (F5 reloads too)", false, true }, { 'r', "rotate", ap_yes, "DEGREES", "turn the whole picture clockwise: 0, 90, 180 or 270", false, true }, { 'S', "sindenedge", ap_yes, "WHERE", "where the Sinden border sits: video or window", false, true }, { 's', "nosound", ap_no, NULL, "mutes all sound", false, true }, { 'T', "patch", ap_yes, "DATABASE", "patch the game DATABASE from the directory or .patch named after the options", false, false }, { 't', "trace", ap_no, NULL, "trace script execution to screen and file", false, true }, { 'U', "unpack", ap_yes, "DATABASE", "unpack the game or patch DATABASE into the directory named after the options", false, false }, { 'u', "stretch", ap_no, NULL, "use ugly stretched video", false, true }, { 'v', "framefile", ap_yes, "FILENAME", "use an alternate video file", false, false }, { 'w', "fullscreen_window", ap_no, NULL, "run in windowed full screen mode", false, true }, { 'X', "shiftx", ap_yes, "PERCENT", "move the picture right (or left, negative) inside the room --scalefactor leaves", false, true }, { 'x', "xresolution", ap_yes, "VALUE", "specify horizontal resolution", false, true }, { 'Y', "shifty", ap_yes, "PERCENT", "move the picture down (or up, negative) inside the room --scalefactor leaves", false, true }, { 'y', "yresolution", ap_yes, "VALUE", "specify vertical resolution", false, true }, { 'z', "noconsole", ap_no, NULL, "zero console output", false, false }, // No short letter left, and one picked at random helps nobody. { OPT_ABSOLUTES_ONLY, "absolutes_only", ap_no, NULL, "keep only the mice that report absolute positions, which is what light guns do", false, true }, { OPT_ALTAUDIO, "altaudio", ap_yes, "SUFFIX", "play .ogg beside the disc video in place of its own audio", false, true }, { OPT_APIVERSION, "apiversion", ap_no, NULL, "print one machine readable version line and exit", false, false }, { OPT_DEINTERLACE, "deinterlace", ap_yes, "MODE", "what to do with an interlaced picture: off, auto or on", false, true }, { OPT_DETERMINISTIC, "deterministic", ap_maybe, "MS", "for testing: ignore real time and move the clock MS milliseconds each frame, seeding the random generators from the same number", false, true }, { OPT_FVALUE, "fvalue", ap_yes, "NUMBER", "one number handed to the game, which reads it with getFValue()", false, true }, { OPT_GAMEPAD_REORDER, "gamepad_reorder", ap_yes, "DIGITS", "which pad fills which slot, as enumeration positions from 0", false, true }, { OPT_HAPTIC, "haptic", ap_yes, "STEP", "strongest rumble step a game may use, 0 to 4; 0 turns rumble off", false, true }, { OPT_IDLEEXIT, "idleexit", ap_yes, "SECONDS", "quit after that long with no input at all; 0 never does", false, true }, { OPT_JOYMOUSE, "joymouse", ap_yes, "BOOLEAN", "let the first gamepad's left stick drive the mouse cursor", false, true }, { OPT_JS_RANGE, "js_range", ap_yes, "SPEED", "how fast that stick drives it, 1 to 20 (default 5)", false, true }, { OPT_KEYMAPFILE, "keymapfile", ap_yes, "FILENAME", "read the control mappings from this file instead of searching for controls.cfg", false, true }, { OPT_LINEARSCALE, "linearscale", ap_yes, "BOOLEAN", "smooth the overlay as the window scales it (the default) or take the nearest pixel", false, true }, { OPT_MANYMOUSE, "manymouse", ap_no, NULL, "tell the mice apart whatever the game asks for, for a cabinet with two guns", false, true }, { OPT_MAPJOYSTICKS, "mapjoysticks", ap_yes, "BOOLEAN", "write a gamepad mapping for a device SDL does not recognise (the default)", false, true }, { OPT_MONOCHROME, "monochrome", ap_no, NULL, "start with the disc picture in grey", false, true }, { OPT_NOGAMEPAD, "nogamepad", ap_no, NULL, "ignore every gamepad, as --nomouse ignores the mice", false, true }, { OPT_SCREEN, "screen", ap_yes, "N", "open the window on display N, counting from 1", false, true }, { OPT_SOUNDFONT, "soundfont", ap_yes, "FILE", "synthesise MIDI files with this SoundFont (.sf2)", false, true }, { OPT_STARTSILENT, "startsilent", ap_no, NULL, "start muted until the first input of any kind", false, true }, { OPT_TRIGGER_THRESHOLD, "trigger_threshold", ap_yes, "PERCENT", "how far a trigger travels before it counts as a button; 0 uses DEAD_ZONE", false, true }, { OPT_XRATIO, "xratio", ap_yes, "FACTOR", "horizontal gun coordinate scale a game reads with ratioGetX()", false, true }, { OPT_YRATIO, "yratio", ap_yes, "FACTOR", "vertical gun coordinate scale a game reads with ratioGetY()", false, true } }; #define OPTION_COUNT (sizeof(_options) / sizeof(_options[0])) // Options the command line carried, so the settings file knows to leave those alone, and what the // settings file did set, for the trace header. static bool _optionSeen[OPTION_COUNT]; static char *_settingsSummary = NULL; // The trace header has to be the first thing in trace.txt, and it cannot be written until the // renderer is chosen, so main.c's own trace lines wait in _tracePending until it has been. static char *_commandLine = NULL; static char *_tracePending = NULL; static bool _traceHeaderDone = false; // Sorted ascending within each ratio; the resolution search relies on that. static const ModeT _modes[] = { { { 4, 3 }, { 640, 480 } }, { { 4, 3 }, { 800, 600 } }, { { 4, 3 }, { 960, 720 } }, { { 4, 3 }, { 1024, 768 } }, { { 4, 3 }, { 1280, 960 } }, { { 4, 3 }, { 1400, 1050 } }, { { 4, 3 }, { 1440, 1080 } }, { { 4, 3 }, { 1600, 1200 } }, { { 4, 3 }, { 1856, 1392 } }, { { 4, 3 }, { 1920, 1440 } }, { { 4, 3 }, { 2048, 1536 } }, { { 16, 9 }, { 1024, 576 } }, { { 16, 9 }, { 1152, 648 } }, { { 16, 9 }, { 1280, 720 } }, { { 16, 9 }, { 1366, 768 } }, { { 16, 9 }, { 1600, 900 } }, { { 16, 9 }, { 1920, 1080 } }, { { 16, 9 }, { 2560, 1440 } }, { { 16, 9 }, { 3840, 2160 } }, { { 16, 9 }, { 7680, 4320 } }, { { 16, 10 }, { 1280, 800 } }, { { 16, 10 }, { 1440, 900 } }, { { 16, 10 }, { 1680, 1050 } }, { { 16, 10 }, { 1920, 1200 } }, { { 16, 10 }, { 2560, 1600 } }, { { 0, 0 }, { 0, 0 } } }; static void _applyOptions(const char *exeName, ConfigT *conf, int32_t argc, const char *argv[], const char *source); static bool _applySetting(const char *exeName, ConfigT *conf, const SettingT *setting); static void _applySettings(const char *exeName, ConfigT *conf); static char *_cloneString(const char *string); #ifndef _WIN32 static void _crashHandler(int signalNumber); #endif static bool _extractFile(const char *filename, const uint8_t *data, size_t length); static char *_findVideoFile(const char *baseName); static void _launcher(const char *exeName, ConfigT *conf); static void _mainTrace(const ConfigT *conf, const char *fmt, ...) __attribute__((format(printf, 2, 3))); static bool _modeMatchesRatio(int32_t index, int32_t ratioIndex); static void _optionFail(const char *exeName, const char *source, const char *message) __attribute__((noreturn)); static int32_t _optionIndex(int32_t code); static int32_t _optionNamed(const char *name); static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[]); static bool _parseBoolean(const char *text, bool *value); static bool _parseGamepadOrder(const char *text); static bool _parseFloat(const char *text, double *value); static bool _parseInteger(const char *text, int32_t *value); static void _requireRange(const char *exeName, const char *source, int32_t value, int32_t min, int32_t max, const char *what, const char *unit); static void _requireRangeFloat(const char *exeName, const char *source, double value, double min, double max, const char *what); static void _resolveFiles(const char *exeName, ConfigT *conf); static bool _runTool(const ConfigT *conf); static void _showApiVersion(void) __attribute__((noreturn)); static void _showHeader(void); static void _showUsage(const char *name, const char *message) __attribute__((noreturn)); static void _startSDL(void); static void _stopSDL(void); static void _traceHeader(const ConfigT *conf, SDL_Renderer *renderer, SDL_GPUDevice *device); static void _unpackData(const char *exePath, bool absolute); // Runs one list of options into a configuration: the command line when source is NULL, or one // line of a settings file, which names itself so an error can point at it. One switch, one set of // checks, whichever the options came from. static void _applyOptions(const char *exeName, ConfigT *conf, int32_t argc, const char *argv[], const char *source) { int32_t x = 0; int32_t argIndex = 0; int32_t code = 0; int32_t index = -1; int32_t aspectNum = -1; int32_t aspectDom = -1; int32_t *target = NULL; double *targetFloat = NULL; char *aspectString = NULL; char *canvasString = NULL; char *sindenString = NULL; char *deinterlaceString = NULL; char *edgeString = NULL; char *temp = NULL; const char *arg = NULL; struct Arg_parser parser; struct ap_Option options[OPTION_COUNT + 1]; // Build the parser table from our option list. for (x = 0; x < (int32_t)OPTION_COUNT; x++) { options[x].code = _options[x].code; options[x].long_name = _options[x].name; options[x].has_arg = _options[x].hasArgument; } options[OPTION_COUNT].code = 0; options[OPTION_COUNT].long_name = NULL; options[OPTION_COUNT].has_arg = ap_no; if (!ap_init(&parser, argc, argv, options, 0)) { utilDie("Out of memory parsing arguments."); } if (ap_error(&parser)) { utilDie("%s", ap_error(&parser)); } // Parse command line for (argIndex = 0; argIndex < ap_arguments(&parser); argIndex++) { code = ap_code(&parser, argIndex); arg = ap_argument(&parser, argIndex); index = _optionIndex(code); target = NULL; targetFloat = NULL; if (index >= 0) { // The command line always beats the settings file, so remember what it carried. _optionSeen[index] |= (source == NULL); } switch (code) { // Non-option: the script file. case 0: if (conf->scriptFile) { _optionFail(exeName, source, "Only one game may be specified."); } conf->scriptFile = strdup(arg); break; // Aspect case 'a': free(aspectString); aspectString = strdup(arg); conf->resolutionWasCalculated = false; break; // Bezel Artwork case 'B': free(conf->bezelFile); conf->bezelFile = strdup(arg); break; // Overscan Zoom case 'b': conf->given |= GIVEN_SCALE; target = &conf->scaleFactor; break; // Show Calculated Frame File Values case 'c': conf->showCalculated = true; break; // Canvas size case 'C': conf->given |= GIVEN_CANVAS; free(canvasString); canvasString = strdup(arg); break; // Laserdisc case 'D': conf->given |= GIVEN_VIDEO; conf->disc = true; break; // Data Dir case 'd': free(conf->dataDir); conf->dataDir = strdup(arg); break; // Effects Volume case 'e': target = &conf->volumeNonVldp; break; // Bezel Artwork In Front Of The Picture case 'F': conf->bezelFlip = true; break; // Full Screen case 'f': conf->fullScreen = true; break; // Audio Delay case 'A': conf->given |= GIVEN_AUDIO_DELAY; target = &conf->audioDelayMs; break; // Sinden Light Gun case 'G': free(conf->gameDir); conf->gameDir = strdup(arg); break; case 'g': conf->given |= GIVEN_SINDEN; free(sindenString); sindenString = strdup(arg); break; // Which entry of a game database to run case 'E': if (!_parseInteger(arg, &conf->entry) || (conf->entry < 1)) { _optionFail(exeName, source, "--entry needs a number from 1."); } break; // Software video decoding case 'H': conf->softwareVideo = true; break; // Packing tools: the second name comes from the script argument case 'P': case 'T': case 'U': conf->toolMode = (code == 'P') ? TOOL_PACK : (code == 'T') ? TOOL_PATCH : TOOL_UNPACK; free(conf->toolSource); conf->toolSource = strdup(arg); break; // Help case 'h': _showUsage(exeName, NULL); break; // Bezel Artwork Folder case 'I': free(conf->bezelDir); conf->bezelDir = strdup(arg); break; // No Logos case 'k': conf->noLogos = true; break; // Video Volume case 'l': target = &conf->volumeVldp; break; // No Mouse case 'm': conf->given |= GIVEN_NO_MOUSE; conf->noMouse = true; break; // No Crosshairs case 'n': conf->noCrosshair = true; break; // Audio Track Output case 'o': conf->given |= GIVEN_AUDIO_TRACK; target = &conf->audioOutputTrack; break; // Program Tracing case 'p': conf->programTracing = true; break; case 'R': conf->reload = true; break; // Presentation Rotation case 'r': conf->given |= GIVEN_ROTATE; target = &conf->rotate; break; // Where The Sinden Border Sits case 'S': free(edgeString); edgeString = strdup(arg); break; // No Sound case 's': conf->noSound = true; break; // Script Tracing case 't': conf->scriptTracing = true; break; // Ugly Stretched Video case 'u': conf->given |= GIVEN_STRETCH; conf->stretchVideo = true; break; // Video File - a video means a disc. case 'v': conf->given |= GIVEN_VIDEO; free(conf->videoFile); conf->videoFile = strdup(arg); conf->disc = true; break; // Full Screen Windowed case 'w': conf->fullScreenWindow = true; break; // Horizontal Shift case 'X': conf->given |= GIVEN_SHIFT; target = &conf->shiftX; break; // X Resolution case 'x': conf->given |= GIVEN_RESOLUTION; target = &conf->xResolution; conf->resolutionWasCalculated = false; break; // Vertical Shift case 'Y': conf->given |= GIVEN_SHIFT; target = &conf->shiftY; break; // Y Resolution case 'y': conf->given |= GIVEN_RESOLUTION; target = &conf->yResolution; conf->resolutionWasCalculated = false; break; // Light Guns Only case OPT_ABSOLUTES_ONLY: conf->absolutesOnly = true; break; // Deinterlacing case OPT_DEINTERLACE: free(deinterlaceString); deinterlaceString = strdup(arg); break; // MIDI Sound Bank case OPT_SOUNDFONT: free(conf->soundfont); conf->soundfont = strdup(arg); utilFixPathSeparators(&conf->soundfont, false); break; // Alternate Disc Audio case OPT_ALTAUDIO: conf->given |= GIVEN_AUDIO_SUFFIX; free(conf->audioSuffix); conf->audioSuffix = strdup(arg); break; // Machine Readable Version case OPT_APIVERSION: _showApiVersion(); break; // Reproducible Runs case OPT_DETERMINISTIC: conf->deterministic = true; if ((arg != NULL) && (arg[0] != 0)) { target = &conf->deterministicStep; } break; // The Launcher's Number case OPT_FVALUE: targetFloat = &conf->fValue; break; // Which Pad Is Player One case OPT_GAMEPAD_REORDER: if (!_parseGamepadOrder(arg)) { _optionFail(exeName, source, "--gamepad_reorder takes the enumeration positions of the pads, from 0, one for each slot: 3210 or 3,2,1,0."); } free(conf->gamepadOrder); conf->gamepadOrder = strdup(arg); break; // Strongest Rumble Step case OPT_HAPTIC: target = &conf->haptic; break; // Attract Timeout case OPT_IDLEEXIT: target = &conf->idleExitSeconds; break; // A Stick For The Mouse case OPT_JOYMOUSE: if (!_parseBoolean(arg, &conf->joyMouse)) { _optionFail(exeName, source, "--joymouse takes true or false."); } break; // How Fast It Drives It case OPT_JS_RANGE: target = &conf->joyMouseRange; break; // Named Control Mappings case OPT_KEYMAPFILE: free(conf->keymapFile); conf->keymapFile = strdup(arg); utilFixPathSeparators(&conf->keymapFile, false); break; // Overlay Filter case OPT_LINEARSCALE: if (!_parseBoolean(arg, &conf->linearScale)) { _optionFail(exeName, source, "--linearscale takes true or false."); } break; // One Mouse Device Per Player case OPT_MANYMOUSE: conf->manyMouse = true; break; // Mappings For Unrecognised Devices case OPT_MAPJOYSTICKS: if (!_parseBoolean(arg, &conf->mapJoysticks)) { _optionFail(exeName, source, "--mapjoysticks takes true or false."); } break; // Grey Disc Picture case OPT_MONOCHROME: conf->monochrome = true; break; // No Gamepads case OPT_NOGAMEPAD: conf->noGamepad = true; break; // Which Display case OPT_SCREEN: if (!_parseInteger(arg, &conf->screen) || (conf->screen < SCREEN_MIN)) { _optionFail(exeName, source, "--screen counts the displays from 1."); } break; // Silent Until Touched case OPT_STARTSILENT: conf->startSilent = true; break; // How Far A Trigger Travels To Count case OPT_TRIGGER_THRESHOLD: targetFloat = &conf->triggerThreshold; break; // Gun Coordinate Scales case OPT_XRATIO: targetFloat = &conf->ratioX; break; case OPT_YRATIO: targetFloat = &conf->ratioY; break; // No console output case 'z': conf->noConsole = true; utilEnableConsole(false); break; default: utilDie("Unknown option code %d.", code); } // Numeric options all validate the same way. if ((target != NULL) && !_parseInteger(arg, target)) { temp = utilCreateString("Bad value for --%s: %s", _options[index].name, arg); _optionFail(exeName, source, temp); } if ((targetFloat != NULL) && !_parseFloat(arg, targetFloat)) { temp = utilCreateString("Bad value for --%s: %s", _options[index].name, arg); _optionFail(exeName, source, temp); } } ap_free(&parser); // A missing script is reported by main() after the support files have // been dealt with: running with no arguments is the documented way to // set up a fresh install. // Do the full screen options make sense? They are the one pair that excludes the other, so // when the command line named one of them it wins over a settings file that named the other; // two on the command line is still the user contradicting themselves. if (conf->fullScreen && conf->fullScreenWindow) { if (_optionSeen[_optionNamed("fullscreen")] && !_optionSeen[_optionNamed("fullscreen_window")]) { conf->fullScreenWindow = false; } else if (_optionSeen[_optionNamed("fullscreen_window")] && !_optionSeen[_optionNamed("fullscreen")]) { conf->fullScreen = false; } else { _optionFail(exeName, source, "Full Screen or Full Screen Windowed. Pick one."); } } // Sane volume, delay and scale values? _requireRange(exeName, source, conf->volumeVldp, VOLUME_MIN, VOLUME_MAX, "Laserdisc volume", "percent"); _requireRange(exeName, source, conf->volumeNonVldp, VOLUME_MIN, VOLUME_MAX, "Effects volume", "percent"); _requireRange(exeName, source, conf->audioDelayMs, -VIDEO_AUDIO_DELAY_MAX, VIDEO_AUDIO_DELAY_MAX, "Audio delay", "milliseconds"); _requireRange(exeName, source, conf->scaleFactor, SCALE_FACTOR_MIN, SCALE_FACTOR_MAX, "Display scale", "percent"); _requireRange(exeName, source, conf->shiftX, SHIFT_MIN, SHIFT_MAX, "Horizontal shift", "percent"); _requireRange(exeName, source, conf->shiftY, SHIFT_MIN, SHIFT_MAX, "Vertical shift", "percent"); _requireRange(exeName, source, conf->rotate, ROTATE_MIN, ROTATE_MAX, "Rotation", "degrees"); _requireRange(exeName, source, conf->haptic, RUMBLE_LEVEL_NONE, RUMBLE_LEVEL_MAX, "Rumble strength", "steps"); _requireRange(exeName, source, conf->idleExitSeconds, IDLE_EXIT_MIN, IDLE_EXIT_MAX, "Idle timeout", "seconds"); _requireRange(exeName, source, conf->joyMouseRange, JOY_MOUSE_RANGE_MIN, JOY_MOUSE_RANGE_MAX, "Joystick mouse speed", "steps"); _requireRange(exeName, source, conf->deterministicStep, DETERMINISTIC_STEP_MIN, DETERMINISTIC_STEP_MAX, "Deterministic step", "milliseconds"); _requireRangeFloat(exeName, source, conf->triggerThreshold, TRIGGER_THRESHOLD_MIN, TRIGGER_THRESHOLD_MAX, "Trigger threshold"); _requireRangeFloat(exeName, source, conf->ratioX, RATIO_MIN, RATIO_MAX, "Horizontal gun ratio"); _requireRangeFloat(exeName, source, conf->ratioY, RATIO_MIN, RATIO_MAX, "Vertical gun ratio"); _requireRangeFloat(exeName, source, conf->fValue, FVALUE_MIN, FVALUE_MAX, "The --fvalue number"); // Hypseus truncates these three as it parses them and hands games the truncated numbers, so // a game comparing what ratioGetX or getFValue answers sees exactly what it saw there. conf->ratioX = floor(conf->ratioX * RATIO_PRECISION) / RATIO_PRECISION; conf->ratioY = floor(conf->ratioY * RATIO_PRECISION) / RATIO_PRECISION; conf->fValue = floor(conf->fValue * FVALUE_PRECISION) / FVALUE_PRECISION; // Only the four right angles: an arbitrary angle would leave the mouse mapping and the // Sinden border with no sensible meaning, and no game asks for one. if ((conf->rotate % ROTATE_STEP) != 0) { _optionFail(exeName, source, "--rotate takes 0, 90, 180 or 270 degrees."); } // A laserdisc rip that kept its interlaced fields combs on a progressive display. Automatic // deinterlacing touches only the frames the file says are interlaced, which is why it is the // default; "on" is for a file whose flags are wrong. if (deinterlaceString) { if (utilStricmp(deinterlaceString, "off") == 0) { conf->deinterlace = DEINTERLACE_OFF; } else if (utilStricmp(deinterlaceString, "auto") == 0) { conf->deinterlace = DEINTERLACE_AUTO; } else if (utilStricmp(deinterlaceString, "on") == 0) { conf->deinterlace = DEINTERLACE_ON; } else { _optionFail(exeName, source, "--deinterlace takes off, auto or on."); } free(deinterlaceString); } // Where does the Sinden border go? Without the option a bezel decides: the gun's camera // sees the whole screen, so artwork around the picture pushes the border out to the window. if (edgeString) { if (utilStricmp(edgeString, "video") == 0) { conf->sindenEdge = SINDEN_EDGE_VIDEO; } else if (utilStricmp(edgeString, "window") == 0) { conf->sindenEdge = SINDEN_EDGE_WINDOW; } else { _optionFail(exeName, source, "--sindenedge takes video or window."); } free(edgeString); } // Sinden light gun? The border is built around whatever the shift and the scale factor // leave, so the two compose and there is nothing to forbid. if (sindenString) { if (!parseSindenString(sindenString, conf)) { _optionFail(exeName, source, "Bad argument count to --sindengun."); } free(sindenString); } // Did they specify an aspect ratio? if (aspectString) { temp = strchr(aspectString, ':'); if (temp != NULL) { *temp = 0; if (!_parseInteger(aspectString, &aspectNum) || !_parseInteger(temp + 1, &aspectDom)) { aspectNum = -1; } } if ((aspectNum > 0) && (aspectDom > 0)) { // Do we understand what they asked for? for (x = 0; _modes[x].ratio.aspectNum != 0; x++) { if ((_modes[x].ratio.aspectNum == aspectNum) && (_modes[x].ratio.aspectDom == aspectDom)) { conf->bestRatioIndex = x; break; } } } if (conf->bestRatioIndex < 0) { _optionFail(exeName, source, "Unknown aspect ratio."); } free(aspectString); } // Did they specify a canvas size? if (canvasString) { temp = strchr(canvasString, 'x'); if (temp == NULL) { _optionFail(exeName, source, "Canvas size must be WIDTHxHEIGHT, for example 640x480."); } *temp = 0; if (!_parseInteger(canvasString, &conf->canvasWidth) || !_parseInteger(temp + 1, &conf->canvasHeight) || (conf->canvasWidth <= 0) || (conf->canvasHeight <= 0)) { _optionFail(exeName, source, "Canvas size must be WIDTHxHEIGHT, for example 640x480."); } free(canvasString); } } // One key of a settings file, applied through the same switch the command line uses so the two // cannot drift apart. Answers whether it took: a key the command line also carried does not. static bool _applySetting(const char *exeName, ConfigT *conf, const SettingT *setting) { const char *argv[2]; char *text = NULL; int32_t index = _optionNamed(setting->key); bool flag = false; if (index < 0) { _optionFail(exeName, setting->source, utilCreateString("%s is not an option.", setting->key)); } if (!_options[index].settable) { _optionFail(exeName, setting->source, utilCreateString("%s cannot be set here; it belongs on the command line.", setting->key)); } if (_optionSeen[index]) { return false; } // carg_parser skips the first entry, as it would the program name. argv[0] = "settings"; if (_options[index].hasArgument == ap_no) { // A switch is written as a boolean here; false simply leaves the built in default alone. if (!_parseBoolean(setting->value, &flag)) { _optionFail(exeName, setting->source, utilCreateString("%s takes true or false.", setting->key)); } if (!flag) { return true; } text = utilCreateString("--%s", setting->key); } else if ((_options[index].hasArgument == ap_maybe) && _parseBoolean(setting->value, &flag)) { // One whose value is optional may be written as a boolean as well as a value. if (!flag) { return true; } text = utilCreateString("--%s", setting->key); } else { text = utilCreateString("--%s=%s", setting->key, setting->value); } argv[1] = text; _applyOptions(exeName, conf, (int32_t)SDL_arraysize(argv), argv, setting->source); free(text); return true; } // The settings file, applied over the built in defaults and under the command line. The GivenE // bits are put back afterwards: a settings file is a set of defaults, not something typed, so a // games.dat entry still overrides what it said exactly as it overrides the built in values. static void _applySettings(const char *exeName, ConfigT *conf) { SettingT *settings = NULL; char *joined = NULL; int32_t count = 0; int32_t x = 0; uint32_t given = conf->given; settings = settingsLoad(conf, &count); for (x = 0; x < count; x++) { if (_applySetting(exeName, conf, &settings[x])) { joined = _settingsSummary; _settingsSummary = utilCreateString("%s%s%s=%s", (joined != NULL) ? joined : "", (joined != NULL) ? " " : "", settings[x].key, settings[x].value); free(joined); } } conf->given = given; settingsFree(settings, count); } static char *_cloneString(const char *string) { if (string == NULL) { return NULL; } return strdup(string); } // Last words on a crash: where it happened, so a report can name the line. Async-signal-unsafe // calls are acceptable here; the process is already lost. #ifndef _WIN32 static void _crashHandler(int signalNumber) { void *frames[CRASH_FRAMES_MAX]; int32_t count = backtrace(frames, CRASH_FRAMES_MAX); fprintf(stderr, "\nSinge crashed (signal %d). Backtrace:\n", signalNumber); backtrace_symbols_fd(frames, count, STDERR_FILENO); fprintf(stderr, "Run with --program and send trace.txt with this.\n"); signal(signalNumber, SIG_DFL); raise(signalNumber); } #endif // Which audio formats this build can decode, for a bug report and so a user whose music is silent // can see at a glance whether its format was ever compiled in. The mixer keeps the list. char *mainDescribeAudioDecoders(void) { char *list = strdup(""); char *grown = NULL; int32_t count = MIX_GetNumAudioDecoders(); int32_t x = 0; for (x = 0; x < count; x++) { grown = utilCreateString("%s%s%s", list, (x > 0) ? ", " : "", MIX_GetAudioDecoder(x)); free(list); list = grown; } return list; } // The processor, for a bug report. Every platform keeps the name somewhere different, and none of // them is worth failing over: the core count is always there as a fallback. char *mainDescribeCpu(void) { #ifdef _WIN32 char name[128]; DWORD bytes = sizeof(name); if (RegGetValueA(HKEY_LOCAL_MACHINE, "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", "ProcessorNameString", RRF_RT_REG_SZ, NULL, name, &bytes) == ERROR_SUCCESS) { return utilCreateString("%s, %d cores, %d MB", name, SDL_GetNumLogicalCPUCores(), SDL_GetSystemRAM()); } #elif defined(__APPLE__) char name[128]; size_t bytes = sizeof(name); if (sysctlbyname("machdep.cpu.brand_string", name, &bytes, NULL, 0) == 0) { return utilCreateString("%s, %d cores, %d MB", name, SDL_GetNumLogicalCPUCores(), SDL_GetSystemRAM()); } #else char line[256]; FILE *info = fopen("/proc/cpuinfo", "r"); char *colon = NULL; char *end = NULL; char *found = NULL; // /proc/cpuinfo calls it "model name" on x86 and "Model" on a Raspberry Pi. The kernel // reports its size as zero, so it is read a line at a time rather than in one piece. while ((info != NULL) && (found == NULL) && (fgets(line, sizeof(line), info) != NULL)) { if (utilStartsWith(line, "model name") || utilStartsWith(line, "Model")) { colon = strchr(line, ':'); if (colon != NULL) { colon++; while (*colon == ' ') { colon++; } for (end = colon + strlen(colon); (end > colon) && ((uint8_t)end[-1] <= ' '); end--) { end[-1] = 0; } found = utilCreateString("%s, %d cores, %d MB", colon, SDL_GetNumLogicalCPUCores(), SDL_GetSystemRAM()); } } } if (info != NULL) { fclose(info); } if (found != NULL) { return found; } #endif return utilCreateString("%s, %d cores, %d MB", SDL_GetPlatform(), SDL_GetNumLogicalCPUCores(), SDL_GetSystemRAM()); } // The operating system and its version, for a bug report. char *mainDescribeOs(void) { #ifdef _WIN32 char release[64]; DWORD bytes = sizeof(release); if (RegGetValueA(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", "CurrentBuild", RRF_RT_REG_SZ, NULL, release, &bytes) == ERROR_SUCCESS) { return utilCreateString("%s build %s", SDL_GetPlatform(), release); } #else struct utsname system; if (uname(&system) == 0) { return utilCreateString("%s %s (%s)", system.sysname, system.release, system.machine); } #endif return strdup(SDL_GetPlatform()); } // Writes an embedded support file, or rewrites it when the installed copy differs from this build's. static bool _extractFile(const char *filename, const uint8_t *data, size_t length) { FILE *out = NULL; char *existing = NULL; size_t bytes = 0; bool written = false; bool same = false; bool existed = utilFileExists(filename); if (existed) { existing = utilReadFile(filename, &bytes); same = (existing != NULL) && (bytes == length) && (memcmp(existing, data, length) == 0); free(existing); if (same) { return false; } } _showHeader(); out = fopen(filename, "wb"); if (!out) { utilDie("Unable to create %s", filename); } written = (fwrite(data, 1, length, out) == length); fclose(out); if (!written) { // Never leave a truncated file behind or it will not be recreated. unlink(filename); utilDie("Unable to write %s", filename); } utilSay(">>> %s File: %s", existed ? "Updated" : "Created", filename); return true; } // Tries every extension libavformat can demux - lower case only, Windows users! static char *_findVideoFile(const char *baseName) { const AVInputFormat *format = NULL; void *opaque = NULL; char *extensions = NULL; char *extension = NULL; char *comma = NULL; char *candidate = NULL; while ((format = av_demuxer_iterate(&opaque)) != NULL) { if (format->extensions == NULL) { continue; } // Extensions are a comma separated list. extensions = strdup(format->extensions); for (extension = extensions; extension != NULL; extension = comma) { comma = strchr(extension, ','); if (comma != NULL) { *comma = 0; comma++; } candidate = utilCreateString("%s.%s", baseName, extension); if (vfsExists(candidate)) { free(extensions); return candidate; } free(candidate); } free(extensions); } return NULL; } static void _launcher(const char *exeName, ConfigT *conf) { int32_t x = 0; int32_t count = 0; int32_t bestResIndex = -1; float thisRatio = 0.0f; float bestRatio = HUGE_VALF; SDL_Window *window = NULL; SDL_Renderer *renderer = NULL; SDL_GPUDevice *device = NULL; SDL_Surface *icon = NULL; MIX_Mixer *mixer = NULL; SDL_DisplayID *displays = NULL; SDL_DisplayID display = 0; const SDL_DisplayMode *mode = NULL; SDL_AudioSpec spec; _traceHeaderDone = false; // Which display? Without --screen, the primary one, as every release has used. display = SDL_GetPrimaryDisplay(); displays = SDL_GetDisplays(&count); if (conf->screen > 0) { if ((displays == NULL) || (conf->screen > count)) { utilSay("There %s %d display%s:", (count == 1) ? "is" : "are", count, (count == 1) ? "" : "s"); for (x = 0; (displays != NULL) && (x < count); x++) { mode = SDL_GetCurrentDisplayMode(displays[x]); utilSay(" --screen %d %s %dx%d", x + SCREEN_MIN, SDL_GetDisplayName(displays[x]), (mode != NULL) ? mode->w : 0, (mode != NULL) ? mode->h : 0); } utilDie("There is no display %d.", conf->screen); } display = displays[conf->screen - SCREEN_MIN]; } SDL_free(displays); // Get current screen resolution mode = SDL_GetCurrentDisplayMode(display); if (mode == NULL) { utilDie("%s", SDL_GetError()); } _mainTrace(conf, "Display %s is %dx%d", SDL_GetDisplayName(display), mode->w, mode->h); // Determine resolution if not specified if ((conf->xResolution <= 0) || (conf->yResolution <= 0)) { _mainTrace(conf, "Determining resolution settings"); if (conf->bestRatioIndex < 0) { // Find our current aspect ratio for (x = 0; _modes[x].ratio.aspectNum != 0; x++) { thisRatio = fabsf(((float)_modes[x].ratio.aspectNum / (float)_modes[x].ratio.aspectDom) - ((float)mode->w / (float)mode->h)); if (thisRatio < bestRatio) { bestRatio = thisRatio; conf->bestRatioIndex = x; } } } _mainTrace(conf, "Aspect ratio is %d:%d", _modes[conf->bestRatioIndex].ratio.aspectNum, _modes[conf->bestRatioIndex].ratio.aspectDom); // Were both resolutions not specified? if ((conf->xResolution <= 0) && (conf->yResolution <= 0)) { // Are we full screen? if (conf->fullScreen || conf->fullScreenWindow) { // Use desktop resolution conf->xResolution = mode->w; conf->yResolution = mode->h; } else { // Find largest window that will fit on the screen but not fill it for (x = 0; _modes[x].ratio.aspectNum != 0; x++) { if (_modeMatchesRatio(x, conf->bestRatioIndex) && (_modes[x].resolution.width < mode->w) && (_modes[x].resolution.height < mode->h)) { bestResIndex = x; } } if (bestResIndex < 0) { _showUsage(exeName, "No window size fits this display. Specify a resolution or use full screen."); } conf->xResolution = _modes[bestResIndex].resolution.width; conf->yResolution = _modes[bestResIndex].resolution.height; } } else { // Find unprovided width/height using provided value for (x = 0; _modes[x].ratio.aspectNum != 0; x++) { if (_modeMatchesRatio(x, conf->bestRatioIndex)) { if ((conf->xResolution > 0) && (_modes[x].resolution.width == conf->xResolution)) { conf->yResolution = _modes[x].resolution.height; break; } if ((conf->yResolution > 0) && (_modes[x].resolution.height == conf->yResolution)) { conf->xResolution = _modes[x].resolution.width; break; } } } } } _mainTrace(conf, "Resolution is %dx%d", conf->xResolution, conf->yResolution); // Did we end up with a valid resolution? if (conf->xResolution <= 0) { _showUsage(exeName, "Unable to determine X resolution. (Is the Y value sane?)"); } if (conf->yResolution <= 0) { _showUsage(exeName, "Unable to determine Y resolution. (Is the X value sane?)"); } if ((conf->xResolution > mode->w) || (conf->yResolution > mode->h)) { _showUsage(exeName, "Specified resolution is larger than the display."); } // Create Window _mainTrace(conf, "Creating window"); window = SDL_CreateWindow("SINGE", conf->xResolution, conf->yResolution, 0); if (window == NULL) { utilDie("%s", SDL_GetError()); } // Centred on the display that was asked for, before any full screen mode is applied, since // full screen takes the display the window is already on. SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED_DISPLAY(display), SDL_WINDOWPOS_CENTERED_DISPLAY(display)); // Window Icon _mainTrace(conf, "Setting icon"); icon = IMG_Load_IO(SDL_IOFromConstMem(icon_png, icon_png_len), true); if (icon == NULL) { utilDie("%s", SDL_GetError()); } SDL_SetWindowIcon(window, icon); SDL_DestroySurface(icon); icon = NULL; // Do we want full screen of some kind? if (conf->fullScreen || conf->fullScreenWindow) { // Exclusive full screen takes the display's current mode; a NULL mode is a borderless desktop window. _mainTrace(conf, "Going fullscreen"); SDL_SetWindowFullscreenMode(window, conf->fullScreen ? mode : NULL); SDL_SetWindowFullscreen(window, true); // Fullscreen is applied asynchronously; wait for it so the renderer sees the final size. SDL_SyncWindow(window); } // Create a renderer. On a GPU device when the platform has one, so the 3D scene can share it; // otherwise SDL's ordinary renderer, which prefers accelerated drivers but can fall back to software. _mainTrace(conf, "Creating renderer"); device = SDL_CreateGPUDevice(SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_DXIL | SDL_GPU_SHADERFORMAT_MSL, false, NULL); if (device == NULL) { _mainTrace(conf, "No SDL_GPU device (%s); trying OpenGL ES", SDL_GetError()); } else { renderer = SDL_CreateGPURenderer(device, window); if (renderer == NULL) { _mainTrace(conf, "GPU renderer failed (%s); trying OpenGL ES", SDL_GetError()); SDL_DestroyGPUDevice(device); device = NULL; } } if (renderer == NULL) { renderer = SDL_CreateRenderer(window, NULL); if (renderer == NULL) { utilDie("%s", SDL_GetError()); } } _mainTrace(conf, "Renderer: %s", SDL_GetRendererName(renderer)); // Tell the render layer which backend the scene and the GUI will be calling through. SDL_GPU // when the platform has it; otherwise OpenGL ES through the context SDL_Renderer just made, // which is the only one a Pi or a Mali handheld offers. Neither leaves 2D untouched and the // scene and the GUI reporting themselves unavailable, as they already did on such a machine. if (device != NULL) { renderSelect(RENDER_GPU, &renderGpuBackend, renderer); } else if (renderGlesStart()) { renderSelect(RENDER_GLES, &renderGlesBackend, renderer); device = rgpuCreateDevice(RGPU_SHADERFORMAT_ESSL, false, NULL); if (device == NULL) { renderSelect(RENDER_NONE, NULL, renderer); } } else { renderSelect(RENDER_NONE, NULL, renderer); } _mainTrace(conf, "Render backend: %s", renderApiName()); // Clear screen with black SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE); SDL_RenderClear(renderer); // Create audio mixer device _mainTrace(conf, "Configuring mixer"); SDL_SetHint(SDL_HINT_AUDIO_DEVICE_SAMPLE_FRAMES, MIXER_CHUNK_SAMPLES); spec.format = SDL_AUDIO_S16; spec.channels = MIXER_CHANNELS; spec.freq = MIXER_FREQUENCY; mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec); if (mixer == NULL) { utilDie("%s", SDL_GetError()); } // How video will be decoded is part of the header, so it is settled before the header is // written; everything else in it is known by now. videoSetHardwareDecoding(!conf->softwareVideo); _traceHeader(conf, renderer, device); // Start our video playback system _mainTrace(conf, "Initializing laserdisc video"); videoInit(mixer); // Finish our setup _mainTrace(conf, "Disabling screen saver"); SDL_DisableScreenSaver(); // Run Singe! _mainTrace(conf, "Starting Singe"); singe(window, renderer, device, conf); // Shutdown - framefiles own video handles, so they go first. _mainTrace(conf, "Shutting down laserdisc framefile handler"); frameFileQuit(); _mainTrace(conf, "Shutting down laserdisc video"); videoQuit(); _mainTrace(conf, "Stopping mixer"); MIX_DestroyMixer(mixer); _mainTrace(conf, "Destroying renderer"); SDL_DestroyRenderer(renderer); if (device != NULL) { rgpuDestroyDevice(device); } _mainTrace(conf, "Destroying window"); SDL_DestroyWindow(window); _mainTrace(conf, "Re-enabling screen saver"); SDL_EnableScreenSaver(); } static void _mainTrace(const ConfigT *conf, const char *fmt, ...) { va_list args; char *line = NULL; char *joined = NULL; if (!conf->programTracing) { return; } va_start(args, fmt); if (_traceHeaderDone) { utilTraceVArgs(fmt, args); } else { line = utilCreateStringVArgs(fmt, args); joined = _tracePending; _tracePending = utilCreateString("%s%s%s", (joined != NULL) ? joined : "", (joined != NULL) ? "\n" : "", line); free(joined); free(line); } va_end(args); } static bool _modeMatchesRatio(int32_t index, int32_t ratioIndex) { return (_modes[index].ratio.aspectNum == _modes[ratioIndex].ratio.aspectNum) && (_modes[index].ratio.aspectDom == _modes[ratioIndex].ratio.aspectDom); } // A bad option. From the command line that is the usage text; from a settings file it names the // file and the key instead, since the usage text would not say which line was wrong. static void _optionFail(const char *exeName, const char *source, const char *message) { if (source == NULL) { _showUsage(exeName, message); } utilDie("%s: %s", source, message); } // Where an option code sits in the table, or -1 for the non-option argument. static int32_t _optionIndex(int32_t code) { int32_t x = 0; for (x = 0; x < (int32_t)OPTION_COUNT; x++) { if (_options[x].code == code) { return x; } } return -1; } // Where a long option name sits in the table, or -1 when there is no such option. static int32_t _optionNamed(const char *name) { int32_t x = 0; for (x = 0; x < (int32_t)OPTION_COUNT; x++) { if (strcmp(_options[x].name, name) == 0) { return x; } } return -1; } static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[]) { ConfigT *conf = (ConfigT *)calloc(1, sizeof(ConfigT)); // Default configuration values if (!conf) { utilDie("Out of memory creating config."); } conf->bestRatioIndex = -1; conf->volumeVldp = VOLUME_MAX; conf->volumeNonVldp = VOLUME_MAX; conf->scaleFactor = SCALE_FACTOR_MAX; conf->resolutionWasCalculated = true; conf->canvasWidth = CANVAS_DEFAULT_WIDTH; conf->canvasHeight = CANVAS_DEFAULT_HEIGHT; conf->entry = 1; conf->haptic = RUMBLE_LEVEL_MAX; conf->linearScale = true; conf->mapJoysticks = true; conf->joyMouseRange = JOY_MOUSE_RANGE_DEFAULT; conf->deinterlace = DEINTERLACE_AUTO; conf->deterministicStep = FRAME_TICK_MS; _applyOptions(exeName, conf, argc, (const char **)argv, NULL); return conf; } // --gamepad_reorder is a list of enumeration positions, one per gamepad slot, written as Hypseus // writes it: bare digits (3210) or separated by commas or spaces (3, 2, 1, 0). A repeated position // is the user contradicting themselves and is refused; anything but a digit or a separator is too. static bool _parseGamepadOrder(const char *text) { bool seen[GAMEPAD_ORDER_DIGITS]; int32_t digits = 0; int32_t x = 0; memset(seen, 0, sizeof(seen)); for (x = 0; text[x] != '\0'; x++) { if ((text[x] == ',') || (text[x] == ' ')) { continue; } if ((text[x] < '0') || (text[x] > '9')) { return false; } if (seen[text[x] - '0']) { return false; } seen[text[x] - '0'] = true; digits++; } return (digits > 0); } // The words a settings file (and --linearscale) may use for a switch. static bool _parseBoolean(const char *text, bool *value) { static const char *yes[] = { "true", "yes", "on", "1" }; static const char *no[] = { "false", "no", "off", "0" }; int32_t x = 0; for (x = 0; x < (int32_t)SDL_arraysize(yes); x++) { if (utilStricmp(text, yes[x]) == 0) { *value = true; return true; } if (utilStricmp(text, no[x]) == 0) { *value = false; return true; } } return false; } static bool _parseFloat(const char *text, double *value) { char *end = NULL; double parsed = 0.0; if ((text == NULL) || (*text == 0)) { return false; } errno = 0; parsed = strtod(text, &end); if ((*end != 0) || (errno == ERANGE)) { return false; } *value = parsed; return true; } static bool _parseInteger(const char *text, int32_t *value) { char *end = NULL; long parsed = 0; if ((text == NULL) || (*text == 0)) { return false; } errno = 0; parsed = strtol(text, &end, 10); if ((*end != 0) || (errno == ERANGE) || (parsed < INT32_MIN) || (parsed > INT32_MAX)) { return false; } *value = (int32_t)parsed; return true; } // Usage error naming the limits, so the message cannot drift from the constants. static void _requireRange(const char *exeName, const char *source, int32_t value, int32_t min, int32_t max, const char *what, const char *unit) { if ((value < min) || (value > max)) { _optionFail(exeName, source, utilCreateString("%s must be between %d and %d %s.", what, min, max, unit)); } } // The same for the three options that carry a fraction. static void _requireRangeFloat(const char *exeName, const char *source, double value, double min, double max, const char *what) { if ((value < min) || (value > max)) { _optionFail(exeName, source, utilCreateString("%s must be between %g and %g.", what, min, max)); } } static void _resolveFiles(const char *exeName, ConfigT *conf) { size_t length = 0; const char *extension = NULL; char *temp = NULL; ConfigT *replacement = NULL; ConfigT swapped; // Exists? A packed game answers through its database. vfsInit(conf->container, conf->dataDirBase, conf->dataDir); midiInit(conf->soundfont); midiIoInit(); videoSetDeinterlace(conf->deinterlace); utilFixPathSeparators(&conf->scriptFile, false); if (!vfsExists(conf->scriptFile)) { // Missing. Is a path? temp = NULL; if (utilPathExists(conf->scriptFile)) { // See if the script named for the path exists inside the path. temp = utilCreateString("%s%c%s.singe", conf->scriptFile, utilGetPathSeparator(), utilGetLastPathComponent(conf->scriptFile)); if (!utilFileExists(temp)) { free(temp); temp = NULL; } } free(conf->scriptFile); conf->scriptFile = temp; } if (!conf->scriptFile) { _showUsage(exeName, "Unable to locate the game."); } // The data directory names the last two places the settings file is looked for, so it is worked // out here; the entry below may move it, and it is worked out again at the end. conf->dataDir = resolveDataDir(conf); if (!conf->dataDir) { _showUsage(exeName, "Unable to create data directory."); } _applySettings(exeName, conf); // A loose script runs with the settings of its games.dat entry, as the menu would run it. if (conf->container == NULL) { replacement = confFromGamesDat(conf); if (replacement != NULL) { swapped = *conf; *conf = *replacement; *replacement = swapped; destroyConf(&replacement); } } // Do we need to generate a video name? if (conf->videoFile) { utilFixPathSeparators(&conf->videoFile, false); if (!vfsExists(conf->videoFile)) { free(conf->videoFile); conf->videoFile = NULL; } } else { // Strip the script's extension (and its dot, when there is one). extension = utilGetFileExtension(conf->scriptFile); length = strlen(conf->scriptFile) - strlen(extension); if (strlen(extension) > 0) { length--; } temp = utilStrndup(conf->scriptFile, length); conf->videoFile = _findVideoFile(temp); // If we still don't have one, try a framefile if (!conf->videoFile) { conf->videoFile = utilCreateString("%s.txt", temp); if (!vfsExists(conf->videoFile)) { free(conf->videoFile); conf->videoFile = NULL; } } free(temp); // A game is only a laserdisc game when it says so. if (!conf->disc && conf->videoFile) { utilSay("Note: %s found but --disc was not given; running without a disc.", conf->videoFile); free(conf->videoFile); conf->videoFile = NULL; } } if (conf->disc && !conf->videoFile) { _showUsage(exeName, "Unable to locate video."); } conf->isFrameFile = conf->disc && isFrameFileName(conf->videoFile); free(conf->dataDir); conf->dataDir = resolveDataDir(conf); if (!conf->dataDir) { _showUsage(exeName, "Unable to create data directory."); } } // --pack, --unpack, and --patch: the option carries the source, the script argument the destination. static bool _runTool(const ConfigT *conf) { switch (conf->toolMode) { case TOOL_PACK: return packGame(conf->toolSource, conf->scriptFile); case TOOL_PATCH: return packPatch(conf->toolSource, conf->scriptFile); case TOOL_UNPACK: return packUnpack(conf->toolSource, conf->scriptFile); default: return false; } } // One line a front end can read, and nothing else on stdout. The contract, which the manual // repeats: the first field is always "singe", the rest are key=value pairs separated by one // space, "protocol" says which version of this line it is, and a reader ignores keys it does not // know so that later versions may add them. static void _showApiVersion(void) { // VERSION_STRING is "vX.YY" for people; a machine wants the number on its own. printf("singe version=%s protocol=%d\n", VERSION_STRING + 1, API_VERSION_PROTOCOL); fflush(stdout); exit(EXIT_SUCCESS); } static void _showHeader(void) { static bool shown = false; if (!shown) { utilRedirectConsole(); // 00000000011111111112222222222333333333344444444445555555555666666666677777777778 // 12345678901234567890123456789012345678901234567890123456789012345678901234567890 utilSay(" ___ ___ _ _ ___ ___"); utilSay("/ __|_ _| \\| |/ __| __| SINGE Is Not a Game Emulator %s", VERSION_STRING); utilSay("\\__ \\| || .` | (_ | _| Copyright (c) 2006-%s Scott C. Duensing", COPYRIGHT_END_YEAR); utilSay("|___/___|_|\\_|\\___|___| https://KangarooPunch.com https://SingeEngine.com"); utilNewline(); shown = true; } } static void _showUsage(const char *name, const char *message) { const int32_t helpColumn = 6 + USAGE_OPTION_WIDTH; // " -x, " plus the padded long form const int32_t helpWidth = USAGE_LINE_WIDTH - helpColumn; const char *help = NULL; const char *end = NULL; const char *space = NULL; int32_t x = 0; char *longForm = NULL; _showHeader(); utilSay("Usage: %s [OPTIONS] gameName", utilGetLastPathComponent(name)); utilSay(" gameName: a .singe script, its directory, or a packed .game file, in the game directory"); utilNewline(); for (x = 0; x < (int32_t)OPTION_COUNT; x++) { if (_options[x].hidden) { continue; } if (_options[x].value == NULL) { longForm = utilCreateString("--%s", _options[x].name); } else if (_options[x].hasArgument == ap_maybe) { // The value may be left off, and the usage text says so. longForm = utilCreateString("--%s[=%s]", _options[x].name, _options[x].value); } else { longForm = utilCreateString("--%s=%s", _options[x].name, _options[x].value); } // Wrap the help at word boundaries; continuation lines start in the help column. help = _options[x].help; while (help != NULL) { end = help + strlen(help); if ((end - help) > helpWidth) { end = help + helpWidth; for (space = end; (space > help) && (*space != ' '); space--) { } if (space > help) { end = space; } } if (help == _options[x].help) { // An option with no short letter starts where the long form does. if (_options[x].code > UINT8_MAX) { utilSay(" %-*s%.*s", USAGE_OPTION_WIDTH, longForm, (int)(end - help), help); } else { utilSay(" -%c, %-*s%.*s", _options[x].code, USAGE_OPTION_WIDTH, longForm, (int)(end - help), help); } } else { utilSay("%*s%.*s", helpColumn, "", (int)(end - help), help); } while (*end == ' ') { end++; } help = (*end != 0) ? end : NULL; } free(longForm); } utilNewline(); if (message) { utilSay("Error: %s", message); utilNewline(); } if (utilGetConsoleEnabled()) { utilWaitForKeyOnWindows(); } exit(message ? EXIT_FAILURE : EXIT_SUCCESS); } static void _startSDL(void) { // Init SDL if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMEPAD | SDL_INIT_EVENTS)) { utilDie("%s", SDL_GetError()); } // Init SDL_mixer (SDL_image needs no init in SDL3) if (!MIX_Init()) { utilDie("%s", SDL_GetError()); } // Init SDL_ttf if (!TTF_Init()) { utilDie("%s", SDL_GetError()); } } static void _stopSDL(void) { TTF_Quit(); MIX_Quit(); SDL_Quit(); } // What a bug report needs, at the top of trace.txt and nowhere else: the build, the command that // started it, the machine, and what the engine picked to run on. main.c's own trace lines wait // behind it so the block is always first and can be pasted whole. static void _traceHeader(const ConfigT *conf, SDL_Renderer *renderer, SDL_GPUDevice *device) { char *os = mainDescribeOs(); char *cpu = mainDescribeCpu(); char *audio = mainDescribeAudioDecoders(); int32_t built = SDL_VERSION; int32_t linked = SDL_GetVersion(); utilTrace("%s", TRACE_HEADER_RULE); utilTrace("Singe: %s", VERSION_STRING); utilTrace("Command: %s", (_commandLine != NULL) ? _commandLine : ""); utilTrace("OS: %s", os); utilTrace("CPU: %s", cpu); utilTrace("Renderer: %s%s%s", SDL_GetRendererName(renderer), (device != NULL) ? ", 3D through " : " (no 3D and no GUI on this machine)", (device != NULL) ? rgpuGetDeviceDriver(device) : ""); utilTrace("Decoder: %s", videoGetDecoderDescription()); utilTrace("Audio: %s", audio); utilTrace("SoundFont: %s", midiSoundfont()); utilTrace("MIDI: %s", midiIoDescription()); utilTrace("SDL: built %d.%d.%d, linked %d.%d.%d", SDL_VERSIONNUM_MAJOR(built), SDL_VERSIONNUM_MINOR(built), SDL_VERSIONNUM_MICRO(built), SDL_VERSIONNUM_MAJOR(linked), SDL_VERSIONNUM_MINOR(linked), SDL_VERSIONNUM_MICRO(linked)); utilTrace("Settings: %s", (_settingsSummary != NULL) ? _settingsSummary : "none"); utilTrace("Game: %s%s%s", conf->scriptFile, (conf->container != NULL) ? " in " : "", (conf->container != NULL) ? conf->container : ""); utilTrace("%s", TRACE_HEADER_RULE); free(os); free(cpu); free(audio); // Everything main.c held back while the header was still being assembled. _traceHeaderDone = true; if (_tracePending != NULL) { utilTrace("%s", _tracePending); free(_tracePending); _tracePending = NULL; } } static void _unpackData(const char *exePath, bool absolute) { const EmbeddedFileT files[] = { { "Framework.singe", Framework_singe, Framework_singe_len }, { "controls.cfg.example", controls_cfg, controls_cfg_len }, { "settings.cfg.example", settings_cfg, settings_cfg_len }, { "Menu.singe", Menu_singe, Menu_singe_len }, { "Tools.singe", Tools_singe, Tools_singe_len }, { "Net.singe", Net_singe, Net_singe_len }, { "Master.singe", Master_singe, Master_singe_len }, { "MenuDocument.singe", MenuDocument_singe, MenuDocument_singe_len }, { "MenuOverlay.singe", MenuOverlay_singe, MenuOverlay_singe_len }, { "Menu.rml", Menu_rml, Menu_rml_len }, { "menu.rcss", menu_rcss, menu_rcss_len }, { "FreeSansBold.ttf", FreeSansBold_ttf, FreeSansBold_ttf_len }, { "menuBackground.mkv", menuBackground_mkv, menuBackground_mkv_len }, { "click.wav", click_wav, click_wav_len }, { "Manual.pdf", Manual_pdf, Manual_pdf_len }, { "gui.rcss", gui_rcss, gui_rcss_len }, { "scoreBezel.rml", scoreBezel_rml, scoreBezel_rml_len }, { "scoreBezel.rcss", scoreBezel_rcss, scoreBezel_rcss_len }, { "subtitle.rml", subtitle_rml, subtitle_rml_len }, { "subtitle.rcss", subtitle_rcss, subtitle_rcss_len } }; int32_t x = 0; char *temp = NULL; char *data = NULL; bool created = false; // Extract missing or outdated support files. We do this here so they are not generated if launched from a front end. if (!utilMkDirP(VFS_ENGINE_DIRECTORY, DIRECTORY_MODE)) { utilDie("Unable to create %s directory.", VFS_ENGINE_DIRECTORY); } for (x = 0; x < (int32_t)(sizeof(files) / sizeof(files[0])); x++) { temp = utilCreateString("%s%c%s", VFS_ENGINE_DIRECTORY, utilGetPathSeparator(), files[x].name); created |= _extractFile(temp, files[x].data, files[x].length); free(temp); } // Script to start menu system. The launcher finds the binary beside itself rather than naming // it, because the binary carries its version and a named one stops working at the next release; // the game launchers in the wild do the same. With --gamedir the binary is elsewhere, so there // is nothing to search for and its full path is written instead. if (utilGetPathSeparator() == '/') { // Unix-ish temp = strdup("Menu.sh"); if (absolute) { data = utilCreateString("#!/usr/bin/env bash\n\ncd \"$(dirname \"$0\")\"\n\"%s\" %s %s/menuBackground.mkv %s/Menu.singe\n", exePath, MENU_OPTIONS, VFS_ENGINE_DIRECTORY, VFS_ENGINE_DIRECTORY); } else { data = utilCreateString("#!/usr/bin/env bash\n\ncd \"$(dirname \"$0\")\"\nSINGE=\nfor f in ./%s*; do\n if [[ -x \"$f\" ]] && [[ -f \"$f\" ]]; then\n SINGE=\"$f\" && break\n fi\ndone\nif [[ -z \"$SINGE\" ]]; then\n echo \"Cannot find the %s program beside this script.\"\n exit 1\nfi\n\"${SINGE}\" %s %s/menuBackground.mkv %s/Menu.singe\n", MENU_BINARY_PREFIX, MENU_BINARY_PREFIX, MENU_OPTIONS, VFS_ENGINE_DIRECTORY, VFS_ENGINE_DIRECTORY); } } else { // Winders temp = strdup("Menu.bat"); if (absolute) { data = utilCreateString("@echo off\r\ncd /d \"%%~dp0\"\r\nstart \"\" \"%s\" %s %s\\menuBackground.mkv %s\\Menu.singe\r\n", exePath, MENU_OPTIONS, VFS_ENGINE_DIRECTORY, VFS_ENGINE_DIRECTORY); } else { data = utilCreateString("@echo off\r\ncd /d \"%%~dp0\"\r\nset \"SINGE=\"\r\nfor /f \"tokens=* usebackq\" %%%%f in (`dir /b %s*.exe`) do (set \"SINGE=%%%%f\" & goto :next)\r\n:next\r\nif not defined SINGE (\r\n echo Cannot find the %s program beside this script.\r\n pause\r\n exit /b 1\r\n)\r\nstart \"\" \"%%SINGE%%\" %s %s\\menuBackground.mkv %s\\Menu.singe\r\n", MENU_BINARY_PREFIX, MENU_BINARY_PREFIX, MENU_OPTIONS, VFS_ENGINE_DIRECTORY, VFS_ENGINE_DIRECTORY); } } created |= _extractFile(temp, (const uint8_t *)data, strlen(data)); utilChMod(temp, SCRIPT_MODE); free(data); free(temp); if (created) { utilNewline(); } } ConfigT *cloneConf(const ConfigT *conf) { ConfigT *c = (ConfigT *)calloc(1, sizeof(ConfigT)); if (!c) { utilDie("Out of memory cloning config."); } // Copy everything, then give the clone its own strings. *c = *conf; c->scriptFile = _cloneString(conf->scriptFile); c->container = _cloneString(conf->container); c->toolSource = _cloneString(conf->toolSource); c->gameDir = _cloneString(conf->gameDir); c->bezelFile = _cloneString(conf->bezelFile); c->bezelDir = _cloneString(conf->bezelDir); c->keymapFile = _cloneString(conf->keymapFile); c->soundfont = _cloneString(conf->soundfont); c->deinterlace = conf->deinterlace; c->audioSuffix = _cloneString(conf->audioSuffix); c->gamepadOrder = _cloneString(conf->gamepadOrder); c->videoFile = _cloneString(conf->videoFile); c->gameId = _cloneString(conf->gameId); c->dataDirBase = _cloneString(conf->dataDirBase); c->dataDir = _cloneString(conf->dataDir); return c; } // Builds and creates dataDirBase + directory of filename. Returns a new string or NULL on failure. char *createDataDir(const char *dataDirBase, const char *filename) { const char separator = utilGetPathSeparator(); char *relative = utilGetUpToLastPathComponent(filename); char *start = relative; char *path = NULL; char *p = NULL; // Keep absolute paths inside the base: drop any drive letter and leading separators. if (isalpha((unsigned char)start[0]) && (start[1] == ':')) { start += 2; } while (*start == separator) { start++; } // Neutralize ".." components. for (p = start; *p != 0; p++) { if ((p[0] == '.') && (p[1] == '.') && ((p == start) || (p[-1] == separator)) && ((p[2] == separator) || (p[2] == 0))) { p[0] = '_'; p[1] = '_'; } } path = utilCreateString("%s%s", dataDirBase, start); free(relative); utilFixPathSeparators(&path, true); // Try to create data directory to ensure it exists. if (!utilMkDirP(path, DIRECTORY_MODE)) { free(path); return NULL; } return path; } // The data directory follows the script's directory; inside a database it sits under the database's // name as well, so two archives holding the same script path never share one. vfs.c's overlay for // a packed game hangs off this directory, so the two cannot disagree. char *createDataDirFor(const ConfigT *conf) { char *stem = NULL; char *name = NULL; char *path = NULL; if (conf->container == NULL) { return createDataDir(conf->dataDirBase, conf->scriptFile); } stem = vfsDatabaseStem(utilGetLastPathComponent(conf->container)); name = utilCreateString("%s%c%s", stem, utilGetPathSeparator(), conf->scriptFile); path = createDataDir(conf->dataDirBase, name); free(name); free(stem); return path; } void destroyConf(ConfigT **confPointer) { ConfigT *conf = *confPointer; if (conf == NULL) { return; } free(conf->gameId); free(conf->dataDir); free(conf->dataDirBase); free(conf->videoFile); free(conf->scriptFile); free(conf->container); free(conf->toolSource); free(conf->gameDir); free(conf->bezelFile); free(conf->bezelDir); free(conf->keymapFile); free(conf->soundfont); free(conf->audioSuffix); free(conf->gamepadOrder); free(conf); *confPointer = NULL; } bool isFrameFileName(const char *filename) { return utilStricmp(utilGetFileExtension(filename), "txt") == 0; } // Ok, this thing can have a mess of different arguments: // WW - Just the width of the white border // WW WB - Width of white border and then black border // RW GW BW WW - Custom color "white" border and width // RW GW BW WW WB - Custom color "white" border and width then width of black border // RW GW BW WW RB GB BB WB - Custom color "white" border and width then custom color "black" border and width bool parseSindenString(const char *sindenString, ConfigT *conf) { const char *p = sindenString; char *end = NULL; long value = 0; conf->sindenArgc = 0; while (*p != 0) { // Skip separators and any quotes the shell left behind. if ((*p == ' ') || (*p == '"') || (*p == '\'')) { p++; continue; } if (conf->sindenArgc >= SINDEN_ARG_MAX) { return false; } value = strtol(p, &end, 10); if (end == p) { return false; } conf->sindenArgv[conf->sindenArgc++] = (int32_t)value; p = end; } // Did we get a sane number of arguments? switch (conf->sindenArgc) { case SINDEN_WHITE: case SINDEN_WHITE_BLACK: case SINDEN_CUSTOM_WHITE: case SINDEN_CUSTOM_WHITE_BLACK: case SINDEN_CUSTOM_WHITE_CUSTOM_BLACK: return true; default: return false; } } void queueScript(const ConfigT *conf) { QueueT *q = (QueueT *)calloc(1, sizeof(QueueT)); if (!q) { utilDie("Out of memory queueing script."); } q->conf = cloneConf(conf); LL_APPEND(_scriptQueue, q); } // Where a game writes: under the -d base (or, for a packed game, the default base) in a directory // named for the game; without either, the game's own folder. A new string, or NULL when it cannot // be created. The one rule for command line, games.dat and scriptPush launches alike. char *resolveDataDir(const ConfigT *conf) { return createDataDirFor(conf); } int main(int argc, char *argv[]) { const char *exeName = argv[0]; int32_t x = 0; char *temp = NULL; char *exePath = NULL; ConfigT *conf = NULL; ConfigT *replacement = NULL; QueueT *q = NULL; bool ok = false; #ifndef _WIN32 signal(SIGSEGV, _crashHandler); signal(SIGBUS, _crashHandler); signal(SIGABRT, _crashHandler); signal(SIGFPE, _crashHandler); #endif // Kept whole for the trace header, because a bug report that does not say how Singe was // started says very little. for (x = 0; x < argc; x++) { temp = _commandLine; _commandLine = utilCreateString("%s%s%s", (temp != NULL) ? temp : "", (temp != NULL) ? " " : "", argv[x]); free(temp); } temp = NULL; // Options first so --help and --noconsole take effect before anything is written. conf = _parseArguments(exeName, argc, argv); // For that dumb OS utilRedirectConsole(); // --gamedir is where the games, the databases and the Singe folder live: it becomes the working // directory, so every relative name (the game, --datadir, the packer's files) counts from there. exePath = utilAbsolutePath(exeName); if (conf->gameDir != NULL) { if (!utilChangeDirectory(conf->gameDir)) { _showUsage(exeName, "Unable to enter the game directory."); } } _unpackData(exePath, conf->gameDir != NULL); free(exePath); // -d names the base under which every game gets a data directory; without it, data/ in the // working directory serves (2.x wrote beside the game, which may not be writable). if (conf->dataDir) { conf->dataDirBase = conf->dataDir; conf->dataDir = NULL; utilFixPathSeparators(&conf->dataDirBase, true); } else { conf->dataDirBase = utilCreateString("data%c", utilGetPathSeparator()); } // The packing tools need no window: run one and leave. if (conf->toolMode != TOOL_NONE) { if (!conf->scriptFile) { _showUsage(exeName, "The packing tools need a second name after the options."); } ok = _runTool(conf); destroyConf(&conf); vfsQuit(); return ok ? EXIT_SUCCESS : EXIT_FAILURE; } // Nothing to run? Installing was the whole job. if (!conf->scriptFile) { _showUsage(exeName, "No game specified."); } // A game database on its own runs its first games.dat entry; a patch is not a game. if (!vfsIsDatabase(conf->scriptFile) && packIsDatabase(conf->scriptFile)) { _showUsage(exeName, "That is a patch database. Apply it with --patch; it cannot be run."); } if (vfsIsDatabase(conf->scriptFile)) { replacement = confFromDatabase(conf); destroyConf(&conf); conf = replacement; } // Queue initial script _resolveFiles(exeName, conf); queueScript(conf); destroyConf(&conf); _startSDL(); // Run script queue while (_scriptQueue) { q = _scriptQueue; // Do they want tracing of any kind? if (q->conf->scriptTracing || q->conf->programTracing) { temp = utilCreateString("%strace.txt", q->conf->dataDir); utilTraceStart(temp); free(temp); } _launcher(exeName, q->conf); destroyConf(&q->conf); LL_DELETE(_scriptQueue, q); free(q); utilTraceEnd(); } _stopSDL(); midiIoQuit(); midiQuit(); vfsQuit(); free(_commandLine); free(_settingsSummary); if (utilGetConsoleEnabled()) { utilWaitForKeyOnWindows(); } return EXIT_SUCCESS; }