diff --git a/CHANGELOG b/CHANGELOG index 3d72bca2d..4d436cd87 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,15 @@ SINGE 3.00 API Changes ----------- +- --gamedir names the directory holding the games, the .game files and + the Singe support folder, so the executable can live anywhere. It + becomes the working directory; relative names count from it. The + Menu.sh the engine writes there runs the executable by its full path. + +- Without --datadir the data directory is now data/ in the game + directory (2.x wrote beside the game, which may be read only). The + menu's Menu.sh no longer needs to pass -d data. + - math.randomseed accepts fractional seeds (floored), so 2.x games that seed with os.clock() products run under Lua 5.4. diff --git a/docs/Manual.adoc b/docs/Manual.adoc index 4a86528b4..0e81e8161 100644 --- a/docs/Manual.adoc +++ b/docs/Manual.adoc @@ -124,10 +124,11 @@ name and any extension FFmpeg can demux, then for a `.txt` framefile. | `-c`, `--showcalculated` | Print the frame ranges of every segment of a framefile, for debugging. | `-C`, `--canvas=WxH` | World size for a game without a disc, default 720x480. Ignored when there is a disc. | `-D`, `--disc` | Play a laserdisc video: the one named by `--framefile`, or the video found next to the script. Implied by `--framefile`. Without it a video next to the script is reported and ignored. -| `-d`, `--datadir=PATHNAME` | Directory for everything Singe writes: video indexes, `trace.txt`, screenshots, the menu's `menu.dat`. A subdirectory named for the game's directory is created inside it (for a packed game, one named for the database and then for the script's directory inside it). Defaults to the game's own directory. +| `-d`, `--datadir=PATHNAME` | Directory for everything Singe writes: video indexes, `trace.txt`, screenshots, saves, the menu's `menu.dat`. A subdirectory named for the game's directory is created inside it (for a packed game, one named for the database and then for the script's directory inside it). Defaults to `data` in the game directory. Singe 2.x wrote beside the game instead; game directories may be read only, so nothing is written there any more. | `-E`, `--entry=N` | Run the Nth entry of the `games.dat` inside a `.game` file (default 1). See <>. | `-e`, `--volume_nonvldp=PERCENT` | Sound effect and extra video volume, `0` to `100`. | `-f`, `--fullscreen` | Exclusive full screen at the desktop resolution. +| `-G`, `--gamedir=PATHNAME` | The directory holding the games, the packed `.game` files and the `Singe` support folder, when the executable lives somewhere else. It becomes the working directory, so the game name, a relative `--datadir` and the packer's names all count from it; absolute paths work as they are. Defaults to the current directory. | `-g`, `--sindengun='PARAMS'` | Draw the border a Sinden light gun tracks and map mouse and light gun positions into the picture inside it: the black ring, when one is given, runs along the edge of the video, the white ring sits inside it, and the game is drawn in the middle. `PARAMS` is one quoted list of numbers: `WW` (white border width in video pixels); `WW WB` (white width, then a black border outside it); `RW GW BW WW` (a custom border color and width); `RW GW BW WW WB` (the same plus a black width); or `RW GW BW WW RB GB BB WB` (custom colors and widths for both). A border that leaves no room for the game (twice the white and black widths together reaching the smaller side of the video), or a negative width, ends the program with a message. Cannot be combined with `--scalefactor`. | `-H`, `--softwarevideo` | Decode video in software even when the platform offers a hardware decoder (VA-API or VDPAU on Linux, D3D11VA on Windows, VideoToolbox on macOS). Use it to rule the hardware path in or out when a video misbehaves; the program trace says which decoder is in use. | `-h`, `--help` | Show the option summary and exit. @@ -297,9 +298,13 @@ teardown that quitting does, anything a script leaks shows up here first. === Game Directory Layout -A Singe installation is a directory containing the Singe executable, the -`Singe` support directory it creates on first run, one directory per game, -and a `data` directory when the menu or a front end passes `--datadir`. +A Singe installation is a game directory containing the `Singe` support +directory the engine creates on first run, one directory per game, any +packed `.game` files, and a `data` directory for everything the games and +the engine write. The executable usually sits there too, but need not: +`--gamedir` names the game directory from anywhere, and the `Menu.sh` (or +`Menu.bat`) the engine writes there then runs the executable by its full +path. ---- Singe/ Support files extracted by the engine @@ -315,7 +320,7 @@ ActionMax/ One game sprite_*.png, sound_*.wav, font_*.ttf DLe.game A game packed into one file (see Single-File Games) data/ - ActionMax/ Indexes, trace.txt, screenshots for that game + ActionMax/ Indexes, trace.txt, screenshots, saves for that game ---- Paths inside a script are relative to the directory Singe was started from, @@ -328,7 +333,10 @@ crosshair = spriteLoad(DIR .. "sprite_Crosshair.png") ---- Everything Singe writes goes to the data directory (`singeGetDataPath()`), -so a game can live on read-only media. +`data/` beside the games unless `--datadir` says otherwise, so a game can +live on read-only media. Singe 2.x wrote saves and settings into the game's +own directory; a game that still does so fails on a read-only install, so +keep every `io.output` under `singeGetDataPath()`. === Packaging Your Game @@ -420,7 +428,7 @@ can be unpacked like a game, but it cannot be run and the menu ignores it. A packed game's data directory is named for the database and then for the script's directory inside it: `DLe/` for a `DLe.game` whose script sits at -its root (or `data/DLe/` under `--datadir=data`), which is where the loose +its root under the data directory (`data/DLe/` by default), which is where the loose `DLe/DLe.singe` has it, so saves and settings are shared between a loose install and a packed one; a script packed in a subdirectory (`Game/Main.singe`) gets `DLe/Game/`. The video index goes there too. @@ -1962,6 +1970,10 @@ changed shape, but a few behaviors did: * Errors inside callbacks now end the game with a traceback instead of being printed and ignored, matching how argument errors have always behaved. * Held keys no longer repeat in `MODE_NORMAL`. +* Nothing is written beside the game. Without `--datadir` the data + directory is `data//` under the game directory, where 2.x used the + game's own directory; saves, settings and high scores belong under + `singeGetDataPath()`. * `math.randomseed` accepts a fractional seed, as it did under 2.10's Lua; it is floored before Lua 5.4's generator sees it. `os.clock` is wall time since the engine started, not processor time. @@ -10354,7 +10366,7 @@ end path = singeGetDataPath() ---- -Returns the directory Singe writes to for this game, with a trailing path separator so a file name can be appended directly. With `--datadir`, or for a packed game, it is a subdirectory of that base named for the game (for a packed game, the database's name and then the script's directory inside it); otherwise it is the game's own directory. Screenshots, video indexes and `trace.txt` go there, and so should save games, high scores and settings. +Returns the directory Singe writes to for this game, with a trailing path separator so a file name can be appended directly. With `--datadir`, or for a packed game, it is a subdirectory of that base named for the game (for a packed game, the database's name and then the script's directory inside it); without `--datadir` the base is `data` in the game directory. Screenshots, video indexes and `trace.txt` go there, and so should save games, high scores and settings; Singe 2.x wrote them beside the game, which may be read only. *Returns:* string path ending in a separator. diff --git a/src/main.c b/src/main.c index e653282a8..fa7bb2309 100644 --- a/src/main.c +++ b/src/main.c @@ -60,7 +60,7 @@ #include "embedded.h" -#define MENU_OPTIONS "-k -w -d data -v" +#define MENU_OPTIONS "-k -w -v" #define PRIMARY_DISPLAY 0 #define MIXER_FREQUENCY 44100 #define MIXER_CHANNELS 2 @@ -121,6 +121,7 @@ static const OptionT _options[] = { { 'e', "volume_nonvldp", ap_yes, "PERCENT", "specify sound effects volume in percent", false }, { 'f', "fullscreen", ap_no, NULL, "run in full screen mode", false }, { 'g', "sindengun", ap_yes, "'PARAMS'", "enable Sinden Light Gun support", false }, + { 'G', "gamedir", ap_yes, "PATHNAME", "directory holding the games and the Singe folder (default: the current one)", false }, { 'H', "softwarevideo", ap_no, NULL, "decode video in software even when a hardware decoder exists", false }, { 'h', "help", ap_no, NULL, "this display", false }, { 'k', "nologos", ap_no, NULL, "kill the splash screens", false }, @@ -194,7 +195,7 @@ 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 _unpackData(const char *name); +static void _unpackData(const char *exePath, bool absolute); static char *_cloneString(const char *string) { @@ -603,6 +604,10 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[]) break; // Sinden Light Gun + case 'G': + free(conf->gameDir); + conf->gameDir = strdup(arg); + break; case 'g': conf->given |= GIVEN_SINDEN; free(sindenString); @@ -956,7 +961,7 @@ static void _showUsage(const char *name, const char *message) { _showHeader(); utilSay("Usage: %s [OPTIONS] gameName", utilGetLastPathComponent(name)); - utilSay(" gameName: a .singe script, its directory, or a packed .game file"); + 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) { @@ -1030,7 +1035,7 @@ static void _stopSDL(void) { } -static void _unpackData(const char *name) { +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 }, @@ -1059,12 +1064,22 @@ static void _unpackData(const char *name) { // Script to start menu system if (utilGetPathSeparator() == '/') { // Unix-ish + // The script runs from the game directory; the binary is named relative to it, or by its + // full path when --gamedir put the games somewhere else. temp = strdup("Menu.sh"); - data = utilCreateString("#!/bin/sh\n\ncd \"$(dirname \"$0\")\"\n./%s %s %s/menuBackground.mkv %s/Menu.singe\n", utilGetLastPathComponent(name), MENU_OPTIONS, VFS_ENGINE_DIRECTORY, VFS_ENGINE_DIRECTORY); + if (absolute) { + data = utilCreateString("#!/bin/sh\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("#!/bin/sh\n\ncd \"$(dirname \"$0\")\"\n./%s %s %s/menuBackground.mkv %s/Menu.singe\n", utilGetLastPathComponent(exePath), MENU_OPTIONS, VFS_ENGINE_DIRECTORY, VFS_ENGINE_DIRECTORY); + } } else { // Winders temp = strdup("Menu.bat"); - data = utilCreateString("@start %s %s %s\\menuBackground.mkv %s\\Menu.singe\n", utilGetLastPathComponent(name), MENU_OPTIONS, VFS_ENGINE_DIRECTORY, VFS_ENGINE_DIRECTORY); + if (absolute) { + data = utilCreateString("@cd /d \"%%~dp0\"\n@start \"\" \"%s\" %s %s\\menuBackground.mkv %s\\Menu.singe\n", exePath, MENU_OPTIONS, VFS_ENGINE_DIRECTORY, VFS_ENGINE_DIRECTORY); + } else { + data = utilCreateString("@start %s %s %s\\menuBackground.mkv %s\\Menu.singe\n", utilGetLastPathComponent(exePath), MENU_OPTIONS, VFS_ENGINE_DIRECTORY, VFS_ENGINE_DIRECTORY); + } } created |= _extractFile(temp, (const uint8_t *)data, strlen(data)); utilChMod(temp, SCRIPT_MODE); @@ -1089,6 +1104,7 @@ ConfigT *cloneConf(const ConfigT *conf) { c->scriptFile = _cloneString(conf->scriptFile); c->container = _cloneString(conf->container); c->toolSource = _cloneString(conf->toolSource); + c->gameDir = _cloneString(conf->gameDir); c->videoFile = _cloneString(conf->videoFile); c->dataDirBase = _cloneString(conf->dataDirBase); c->dataDir = _cloneString(conf->dataDir); @@ -1166,6 +1182,7 @@ void destroyConf(ConfigT **confPointer) { free(conf->scriptFile); free(conf->container); free(conf->toolSource); + free(conf->gameDir); free(conf); *confPointer = NULL; } @@ -1235,17 +1252,14 @@ void queueScript(const ConfigT *conf) { // 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) { - if (conf->dataDirGiven || (conf->container != NULL)) { - return createDataDirFor(conf); - } - - return utilGetUpToLastPathComponent(conf->scriptFile); + return createDataDirFor(conf); } int main(int argc, char *argv[]) { const char *exeName = argv[0]; char *temp = NULL; + char *exePath = NULL; ConfigT *conf = NULL; ConfigT *replacement = NULL; QueueT *q = NULL; @@ -1264,16 +1278,26 @@ int main(int argc, char *argv[]) { // For that dumb OS utilRedirectConsole(); - _unpackData(exeName); + // --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."); + } + } - // -d names the base under which every game gets a data directory; without it the game folder serves. + _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; - conf->dataDirGiven = true; + conf->dataDirBase = conf->dataDir; + conf->dataDir = NULL; utilFixPathSeparators(&conf->dataDirBase, true); } else { - conf->dataDirBase = utilCreateString(".%c", utilGetPathSeparator()); + conf->dataDirBase = utilCreateString("data%c", utilGetPathSeparator()); } // The packing tools need no window: run one and leave. diff --git a/src/singe.h b/src/singe.h index 0142ed114..d10025c47 100644 --- a/src/singe.h +++ b/src/singe.h @@ -71,7 +71,7 @@ typedef struct ConfigS { char *toolSource; // Argument of --pack, --unpack, or --patch ToolModeE toolMode; uint32_t given; // GivenE bits - bool dataDirGiven; // -d was on the command line + char *gameDir; // -G: where the games and the Singe folder live; becomes the working directory char *dataDir; char *dataDirBase; bool resolutionWasCalculated; diff --git a/src/util.c b/src/util.c index 54d50652b..517f3a152 100644 --- a/src/util.c +++ b/src/util.c @@ -26,12 +26,16 @@ #include #include #include +#include #define ourMkdir(p,m) mkdir(p) +#define ourChdir _chdir #define CONSOLE_LINES 1000 #else +#include #define ourMkdir mkdir +#define ourChdir chdir #endif @@ -71,6 +75,28 @@ static void _printLine(FILE *stream, const char *fmt, va_list args) { } +// The absolute form of a path, or a copy of it when the system cannot resolve it. Caller frees. +char *utilAbsolutePath(const char *path) { + char *resolved = NULL; + +#ifdef _WIN32 + resolved = _fullpath(NULL, path, 0); +#else + resolved = realpath(path, NULL); +#endif + if (resolved == NULL) { + resolved = strdup(path); + } + + return resolved; +} + + +bool utilChangeDirectory(const char *path) { + return (ourChdir(path) == 0); +} + + bool utilChMod(const char *path, const mode_t mode) { bool result = true; diff --git a/src/util.h b/src/util.h index b70508f6f..0b6b70b7e 100644 --- a/src/util.h +++ b/src/util.h @@ -36,6 +36,8 @@ #define UTIL_PATH_MAX 1024 +char *utilAbsolutePath(const char *path); +bool utilChangeDirectory(const char *path); bool utilChMod(const char *path, const mode_t mode); char *utilCreateString(const char *format, ...) __attribute__((format(printf, 1, 2))); char *utilCreateStringVArgs(const char *format, va_list args) __attribute__((format(printf, 1, 0)));