2173 lines
75 KiB
C
2173 lines
75 KiB
C
/*
|
|
*
|
|
* Singe 3
|
|
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
|
*
|
|
* 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 <stdio.h>
|
|
#include <errno.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <ctype.h>
|
|
#include <math.h>
|
|
#include <sys/stat.h>
|
|
#ifdef _WIN32
|
|
// _describeCpu reads the processor name out of the registry, which needs the Windows headers.
|
|
#include <windows.h>
|
|
#include <io.h>
|
|
#else
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#include <signal.h>
|
|
#ifndef _WIN32
|
|
#include <execinfo.h>
|
|
#include <sys/utsname.h>
|
|
#endif
|
|
#ifdef __APPLE__
|
|
#include <sys/sysctl.h>
|
|
#endif
|
|
|
|
#include <SDL3/SDL.h>
|
|
#include <SDL3/SDL_main.h>
|
|
#include <SDL3_image/SDL_image.h>
|
|
#include <SDL3_mixer/SDL_mixer.h>
|
|
#include <SDL3_ttf/SDL_ttf.h>
|
|
#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 <base><SUFFIX>.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;
|
|
}
|