Can now move the game directory.

This commit is contained in:
Scott Duensing 2026-09-07 20:09:28 -05:00
parent 8fab74e977
commit 7578d21171
6 changed files with 99 additions and 26 deletions

View file

@ -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.

View file

@ -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 <<singlefile,Single-File Games>>.
| `-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/<game>/` 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.

View file

@ -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.

View file

@ -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;

View file

@ -26,12 +26,16 @@
#include <stdio.h>
#include <fcntl.h>
#include <io.h>
#include <direct.h>
#define ourMkdir(p,m) mkdir(p)
#define ourChdir _chdir
#define CONSOLE_LINES 1000
#else
#include <unistd.h>
#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;

View file

@ -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)));