Game patches supported.

This commit is contained in:
Scott Duensing 2026-09-04 13:48:00 -05:00
parent 84a2c25183
commit 6859990bec
7 changed files with 194 additions and 68 deletions

View file

@ -49,14 +49,14 @@ API Changes
LEGACY_SPRITE_ARGS = true to their games.dat entry. See "Migrating from LEGACY_SPRITE_ARGS = true to their games.dat entry. See "Migrating from
Singe 2.10" in the manual. Singe 2.10" in the manual.
- Singe is no longer laserdisc only. A game declares a disc with - Singe is no longer laserdisc only. A games.dat entry with a VIDEO is a
DISC = true in games.dat (or --disc / --framefile on the command line); laserdisc game; one with the line missing or blank runs without a
otherwise it runs without one, on a canvas sized by CANVAS_X/CANVAS_Y or disc, on a canvas sized by CANVAS_X/CANVAS_Y (default 720x480), the
--canvas=WxH (default 720x480), the disc functions become harmless, and disc functions become harmless, and discGetState() returns the new
discGetState() returns the new DISC_EJECTED. DISC_EJECTED. On the command line --disc (implied by --framefile)
Every existing games.dat entry needs DISC = true added; the menu skips declares the disc, since a video found beside the script no longer
an entry with VIDEO but no DISC = true and says why, and the engine does; --canvas=WxH sizes the canvas. Existing games.dat files need no
refuses one if launched directly. Scripts can test SINGE_DISC. change. Scripts can test SINGE_DISC.
- mouseSetEnabled(bool) and singeSetPauseKeyEnabled(bool) added. The old - mouseSetEnabled(bool) and singeSetPauseKeyEnabled(bool) added. The old
mouseEnable/mouseDisable and singeEnablePauseKey/singeDisablePauseKey are mouseEnable/mouseDisable and singeEnablePauseKey/singeDisablePauseKey are

View file

@ -419,26 +419,17 @@ local function loadGamesDat(source, container)
return return
end end
for _, value in pairs(GAMES or {}) do for _, value in pairs(GAMES or {}) do
-- Since 3.00 a laserdisc game must say DISC = true; refuse the ambiguous cases here if container then
-- with a message rather than letting the engine stop the menu when it is picked. -- Packed game: the engine opens its files through the database, and so does the menu.
local title = tostring(value.TITLE or value.SCRIPT or "?") value.CONTAINER = container
if value.VIDEO and not value.DISC then for _, key in ipairs({ "CABINET", "MARQUEE", "ATTRACT" }) do
debugPrint(source .. ": \"" .. title .. "\" names a VIDEO without DISC = true; add DISC = true to the entry. Skipped.") if value[key] then
elseif value.DISC and not value.VIDEO then value[key] = container .. "/" .. value[key]
debugPrint(source .. ": \"" .. title .. "\" says DISC = true but has no VIDEO. Skipped.")
else
if container then
-- Packed game: the engine opens its files through the database, and so does the menu.
value.CONTAINER = container
for _, key in ipairs({ "CABINET", "MARQUEE", "ATTRACT" }) do
if value[key] then
value[key] = container .. "/" .. value[key]
end
end end
end end
table.insert(GAME_LIST, value)
GAME_COUNT = GAME_COUNT + 1
end end
table.insert(GAME_LIST, value)
GAME_COUNT = GAME_COUNT + 1
end end
GAMES = {} GAMES = {}
end end

View file

@ -353,13 +353,20 @@ rule ambiguous), or whose scripts and data files reach outside the game
with `..`; it reports the file and line. Two files whose names differ only with `..`; it reports the file and line. Two files whose names differ only
by case cannot both be packed. Stale `.index` files are skipped. A game by case cannot both be packed. Stale `.index` files are skipped. A game
that references another game's directory (a shared framework beside it) that references another game's directory (a shared framework beside it)
must copy that directory inside first. must copy that directory inside first. Before the database is committed,
its `games.dat` is checked: every entry needs a `SCRIPT`, and each
`SCRIPT`, `VIDEO`, `CABINET`, `MARQUEE`, and `ATTRACT` it names must be a
file in the database (or under `Singe/`); a bad path is reported with the
entry's title and the pack is refused. `--patch` runs the same check on the
patched game, so a patch cannot leave a dangling path behind either.
`--patch` takes either a directory laid out like the game, whose files `--patch` takes either a directory laid out like the game, whose files
replace the packed ones, or a database made by `--pack` from such a replace the packed ones, or a `.patch` file made by `--pack` from such a
directory. Either way the change is one transaction: the game is intact if directory (`Singe --pack fixes DLe-1.1.patch`; a patch needs no
it is interrupted. A patch database may also carry a `removed(path)` table `games.dat`). Files the patch does not name are left alone. Either way the
naming files to delete. change is one transaction: the game is intact if it is interrupted. A patch
may also carry a `removed(path)` table naming files to delete. A `.patch`
can be unpacked like a game, but it cannot be run and the menu ignores it.
A packed game keeps its data directory exactly where the loose game would A packed game keeps its data directory exactly where the loose game would
have it (`data/DLe/` for `DLe/DLe.singe`), so saves and settings are shared have it (`data/DLe/` for `DLe/DLe.singe`), so saves and settings are shared
@ -615,7 +622,6 @@ GAMES = {
{ {
TITLE = ".38 Ambush Alley", TITLE = ".38 Ambush Alley",
SCRIPT = "ActionMax/38AmbushAlley.singe", SCRIPT = "ActionMax/38AmbushAlley.singe",
DISC = true,
VIDEO = "ActionMax/frame_38AmbushAlley.txt", VIDEO = "ActionMax/frame_38AmbushAlley.txt",
DATA = "ActionMax", DATA = "ActionMax",
STRETCH = false, STRETCH = false,
@ -641,14 +647,13 @@ GAMES = {
} }
---- ----
The keys `SCRIPT`, `DISC`, `VIDEO`, `CANVAS_X`, `CANVAS_Y`, `STRETCH`, The keys `SCRIPT`, `VIDEO`, `CANVAS_X`, `CANVAS_Y`, `STRETCH`,
`NO_MOUSE`, `RESOLUTION_X`, `RESOLUTION_Y`, `SINDEN_GUN`, `AUDIO_TRACK`, `NO_MOUSE`, `RESOLUTION_X`, `RESOLUTION_Y`, `SINDEN_GUN`, `AUDIO_TRACK`,
`AUDIO_DELAY`, and `LEGACY_SPRITE_ARGS` are read by the engine when the menu (or your own `AUDIO_DELAY`, and `LEGACY_SPRITE_ARGS` are read by the engine when the menu (or your own
script, through `scriptExecute` / `scriptPush`) launches the entry; they script, through `scriptExecute` / `scriptPush`) launches the entry; they
override the command line. A laserdisc game must say `DISC = true` and name override the command line. The `VIDEO` line is the disc: an entry that names
its `VIDEO`; an entry with a `VIDEO` but no `DISC = true` is refused with a one is a laserdisc game, and an entry that leaves it out or blank runs
message, as is `DISC = true` without a `VIDEO`. A game without a disc leaves without a disc and may set `CANVAS_X` / `CANVAS_Y` (default 720x480); see
both out and may set `CANVAS_X` / `CANVAS_Y` (default 720x480); see
<<withoutadisc,Games Without a Disc>>. `LEGACY_SPRITE_ARGS = true` runs a game written for Singe 2.10 <<withoutadisc,Games Without a Disc>>. `LEGACY_SPRITE_ARGS = true` runs a game written for Singe 2.10
with the old sprite argument order (see <<migrating,Migrating from Singe with the old sprite argument order (see <<migrating,Migrating from Singe
2.10>>). The remaining keys are read by the menu for display. 2.10>>). The remaining keys are read by the menu for display.
@ -657,7 +662,7 @@ with the old sprite argument order (see <<migrating,Migrating from Singe
=== Games Without a Disc === Games Without a Disc
Singe no longer assumes a laserdisc. A game is a laserdisc game only when it Singe no longer assumes a laserdisc. A game is a laserdisc game only when it
says so: `DISC = true` in its `games.dat` entry, or `--disc` (or a names one: a `VIDEO` in its `games.dat` entry, or `--disc` (or a
`--framefile`) on the command line. Everything else runs without a disc, and `--framefile`) on the command line. Everything else runs without a disc, and
the world is a *canvas* instead of a video frame: `CANVAS_X` / `CANVAS_Y` in the world is a *canvas* instead of a video frame: `CANVAS_X` / `CANVAS_Y` in
`games.dat` or `--canvas=WxH` on the command line, 720x480 by default so a `games.dat` or `--canvas=WxH` on the command line, 720x480 by default so a
@ -715,11 +720,11 @@ changed shape, but a few behaviors did:
* `mouseEnable` / `mouseDisable` and `singeEnablePauseKey` / * `mouseEnable` / `mouseDisable` and `singeEnablePauseKey` /
`singeDisablePauseKey` still work, as aliases of `mouseSetEnabled` and `singeDisablePauseKey` still work, as aliases of `mouseSetEnabled` and
`singeSetPauseKeyEnabled`. `singeSetPauseKeyEnabled`.
* A laserdisc game must now declare itself: add `DISC = true` next to * A laserdisc is no longer assumed. A `games.dat` entry with a `VIDEO` is a
`VIDEO` in every `games.dat` entry, and pass `--disc` (or `--framefile`) laserdisc game, as before; one without runs on a canvas instead. On the
on the command line. Without it the game runs without a disc, and a command line pass `--disc` (or `--framefile`), since a video found beside
`games.dat` entry that names a `VIDEO` without `DISC = true` is refused the script no longer implies a disc. See <<withoutadisc,Games Without a
with a message naming the fix. See <<withoutadisc,Games Without a Disc>>. Disc>>.
* The pause key now pauses the whole game, not just the media: the script * The pause key now pauses the whole game, not just the media: the script
is frozen until the key is pressed again, and `SWITCH_PAUSE` is delivered is frozen until the key is pressed again, and `SWITCH_PAUSE` is delivered
to the script only when the key has been disabled. It acts on the key to the script only when the key has been disabled. It acts on the key

View file

@ -127,12 +127,12 @@ static const OptionT _options[] = {
{ 'm', "nomouse", ap_no, NULL, "disable mouse", false }, { 'm', "nomouse", ap_no, NULL, "disable mouse", false },
{ 'n', "nocrosshair", ap_no, NULL, "request game not display gun crosshairs", false }, { 'n', "nocrosshair", ap_no, NULL, "request game not display gun crosshairs", false },
{ 'o', "audio", ap_yes, "TRACK", "select default track for audio output", false }, { 'o', "audio", ap_yes, "TRACK", "select default track for audio output", false },
{ 'P', "pack", ap_yes, "DIRECTORY", "pack a game DIRECTORY into the database named after the options", false }, { 'P', "pack", ap_yes, "DIRECTORY", "pack a game DIRECTORY into the .game (or changed files into the .patch) named after the options", false },
{ 'p', "program", ap_no, NULL, "trace Singe execution to screen and file", false }, { 'p', "program", ap_no, NULL, "trace Singe execution to screen and file", false },
{ 's', "nosound", ap_no, NULL, "mutes all sound", false }, { 's', "nosound", ap_no, NULL, "mutes all sound", false },
{ 'T', "patch", ap_yes, "DATABASE", "patch a game DATABASE from the directory or patch database named after the options", false }, { 'T', "patch", ap_yes, "DATABASE", "patch a game DATABASE from the directory or .patch named after the options", false },
{ 't', "trace", ap_no, NULL, "trace script execution to screen and file", false }, { 't', "trace", ap_no, NULL, "trace script execution to screen and file", false },
{ 'U', "unpack", ap_yes, "DATABASE", "unpack a game DATABASE into the directory named after the options", false }, { 'U', "unpack", ap_yes, "DATABASE", "unpack a game or patch DATABASE into the directory named after the options", false },
{ 'u', "stretch", ap_no, NULL, "use ugly stretched video", false }, { 'u', "stretch", ap_no, NULL, "use ugly stretched video", false },
{ 'v', "framefile", ap_yes, "FILENAME", "use an alternate video file", false }, { 'v', "framefile", ap_yes, "FILENAME", "use an alternate video file", false },
{ 'w', "fullscreen_window", ap_no, NULL, "run in windowed full screen mode", false }, { 'w', "fullscreen_window", ap_no, NULL, "run in windowed full screen mode", false },
@ -1203,7 +1203,10 @@ int main(int argc, char *argv[]) {
_showUsage(exeName, "No script file specified."); _showUsage(exeName, "No script file specified.");
} }
// A game database on its own runs its first games.dat entry. // 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)) { if (vfsIsDatabase(conf->scriptFile)) {
replacement = confFromDatabase(conf); replacement = confFromDatabase(conf);
destroyConf(&conf); destroyConf(&conf);

View file

@ -12,13 +12,20 @@
#include "sqlite3.h" #include "sqlite3.h"
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"
#include "pack.h" #include "pack.h"
#include "singe.h" #include "singe.h"
#include "util.h" #include "util.h"
#include "vfs.h" #include "vfs.h"
#define PAGE_SIZE 4096 #define ENGINE_PREFIX "singe/"
#define GAMES_DAT "games.dat"
#define PAGE_SIZE 4096
#define PATCH_EXTENSION "patch"
typedef struct EntryS { typedef struct EntryS {
@ -52,8 +59,10 @@ static void _entriesFree(EntryT *list);
static bool _exec(sqlite3 *db, const char *sql); static bool _exec(sqlite3 *db, const char *sql);
static bool _insertFile(WriterT *writer, const EntryT *entry); static bool _insertFile(WriterT *writer, const EntryT *entry);
static bool _isDirectory(const char *path); static bool _isDirectory(const char *path);
static bool _isPatchName(const char *database);
static char *_keyFor(const char *relative); static char *_keyFor(const char *relative);
static bool _scanEscapes(const EntryT *list); static bool _scanEscapes(const EntryT *list);
static bool _validateGamesDat(WriterT *writer);
static bool _writeMeta(sqlite3 *db, const char *key, const char *value); static bool _writeMeta(sqlite3 *db, const char *key, const char *value);
static bool _writerClose(WriterT *writer, bool commit); static bool _writerClose(WriterT *writer, bool commit);
static bool _writerOpen(WriterT *writer, const char *database, bool create); static bool _writerOpen(WriterT *writer, const char *database, bool create);
@ -103,7 +112,8 @@ static bool _applyDatabase(WriterT *writer, const char *source) {
} }
// Inserts every file below directory. checkGame enforces the rules a whole game must meet. // Inserts every file below directory. checkGame enforces the rules a whole game must meet; the
// forbidden-file list and the escape scan apply to patches too.
static bool _applyDirectory(WriterT *writer, const char *directory, bool checkGame) { static bool _applyDirectory(WriterT *writer, const char *directory, bool checkGame) {
EntryT *list = NULL; EntryT *list = NULL;
EntryT *entry = NULL; EntryT *entry = NULL;
@ -134,7 +144,7 @@ static bool _applyDirectory(WriterT *writer, const char *directory, bool checkGa
} }
} }
} }
if (ok && checkGame && !_scanEscapes(list)) { if (ok && !_scanEscapes(list)) {
ok = false; ok = false;
} }
for (entry = list; ok && (entry != NULL); entry = entry->next) { for (entry = list; ok && (entry != NULL); entry = entry->next) {
@ -296,6 +306,12 @@ static bool _isDirectory(const char *path) {
} }
// Patch databases are .patch files: packed like a game but without a games.dat, and never run.
static bool _isPatchName(const char *database) {
return utilStricmp(utilGetFileExtension(database), PATCH_EXTENSION) == 0;
}
// The lookup key: lower case, forward slashes. // The lookup key: lower case, forward slashes.
static char *_keyFor(const char *relative) { static char *_keyFor(const char *relative) {
char *key = strdup(relative); char *key = strdup(relative);
@ -352,6 +368,101 @@ static bool _scanEscapes(const EntryT *list) {
} }
// Every file a games.dat entry names must be in the database (or belong to the engine), checked
// before the transaction commits so a game or patch never ships with a dangling path.
static bool _validateGamesDat(WriterT *writer) {
static const char *const keys[] = { "SCRIPT", "VIDEO", "CABINET", "MARQUEE", "ATTRACT", NULL };
sqlite3_stmt *stmt = NULL;
lua_State *L = NULL;
char *gameDir = NULL;
char *key = NULL;
const char *value = NULL;
const char *title = NULL;
const char *inner = NULL;
size_t length = 0;
int32_t x = 0;
bool ok = true;
// The file itself, and the own-directory prefix its paths may carry.
if ((sqlite3_prepare_v2(writer->db, "SELECT data FROM assets WHERE path = ?", -1, &stmt, NULL) != SQLITE_OK)) {
return false;
}
sqlite3_bind_text(stmt, 1, GAMES_DAT, -1, SQLITE_STATIC);
if (sqlite3_step(stmt) != SQLITE_ROW) {
sqlite3_finalize(stmt);
utilSay("!!! No %s at the root of the game.", GAMES_DAT);
return false;
}
L = luaL_newstate();
luaL_openlibs(L);
if ((luaL_loadbuffer(L, sqlite3_column_blob(stmt, 0), (size_t)sqlite3_column_bytes(stmt, 0), GAMES_DAT) != LUA_OK) || (lua_pcall(L, 0, 0, 0) != LUA_OK)) {
utilSay("!!! %s", lua_tostring(L, -1));
sqlite3_finalize(stmt);
lua_close(L);
return false;
}
sqlite3_finalize(stmt);
if (sqlite3_prepare_v2(writer->db, "SELECT value FROM meta WHERE key = 'gamedir'", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
gameDir = _keyFor((const char *)sqlite3_column_text(stmt, 0));
}
sqlite3_finalize(stmt);
}
if (sqlite3_prepare_v2(writer->db, "SELECT count(*) FROM assets WHERE path = ?", -1, &stmt, NULL) != SQLITE_OK) {
free(gameDir);
lua_close(L);
return false;
}
lua_getglobal(L, "GAMES");
if (!lua_istable(L, -1)) {
utilSay("!!! %s defines no GAMES table.", GAMES_DAT);
ok = false;
}
lua_pushnil(L);
while (ok && lua_next(L, -2)) {
if (lua_istable(L, -1)) {
lua_getfield(L, -1, "TITLE");
title = lua_tostring(L, -1) ? lua_tostring(L, -1) : "(untitled)";
for (x = 0; keys[x] != NULL; x++) {
lua_getfield(L, -2, keys[x]);
value = lua_tostring(L, -1);
if ((value != NULL) && (value[0] != 0)) {
key = _keyFor(value);
inner = key;
if (gameDir != NULL) {
length = strlen(gameDir);
if ((strncmp(inner, gameDir, length) == 0) && (inner[length] == '/')) {
inner += length + 1;
}
}
if (strncmp(key, ENGINE_PREFIX, strlen(ENGINE_PREFIX)) != 0) {
sqlite3_reset(stmt);
sqlite3_bind_text(stmt, 1, inner, -1, SQLITE_TRANSIENT);
if ((sqlite3_step(stmt) != SQLITE_ROW) || (sqlite3_column_int64(stmt, 0) == 0)) {
utilSay("!!! %s: \"%s\" names %s = \"%s\", which is not in the game.", GAMES_DAT, title, keys[x], value);
ok = false;
}
}
free(key);
} else if (strcmp(keys[x], "SCRIPT") == 0) {
utilSay("!!! %s: \"%s\" has no SCRIPT.", GAMES_DAT, title);
ok = false;
}
lua_pop(L, 1);
}
lua_pop(L, 1);
}
lua_pop(L, 1);
}
sqlite3_finalize(stmt);
free(gameDir);
lua_close(L);
return ok;
}
static bool _writeMeta(sqlite3 *db, const char *key, const char *value) { static bool _writeMeta(sqlite3 *db, const char *key, const char *value) {
sqlite3_stmt *stmt = NULL; sqlite3_stmt *stmt = NULL;
bool ok = false; bool ok = false;
@ -467,13 +578,14 @@ char *packForbiddenReason(const char *path, bool isPatch) {
} }
// Packs a game directory into a new database. // Packs a game directory into a new database, or a directory of changed files into a .patch.
bool packGame(const char *directory, const char *database) { bool packGame(const char *directory, const char *database) {
WriterT writer; WriterT writer;
char *root = strdup(directory); char *root = strdup(directory);
char *gamesDat = NULL; char *gamesDat = NULL;
char *chunk = NULL; char *chunk = NULL;
size_t length = strlen(root); size_t length = strlen(root);
bool isPatch = _isPatchName(database);
bool ok = true; bool ok = true;
while ((length > 1) && ((root[length - 1] == '/') || (root[length - 1] == '\\'))) { while ((length > 1) && ((root[length - 1] == '/') || (root[length - 1] == '\\'))) {
@ -485,7 +597,7 @@ bool packGame(const char *directory, const char *database) {
return false; return false;
} }
gamesDat = utilCreateString("%s/games.dat", root); gamesDat = utilCreateString("%s/games.dat", root);
if (!utilFileExists(gamesDat)) { if (!isPatch && !utilFileExists(gamesDat)) {
utilSay("!!! %s has no games.dat; a game database needs one at its root.", root); utilSay("!!! %s has no games.dat; a game database needs one at its root.", root);
free(gamesDat); free(gamesDat);
free(root); free(root);
@ -502,7 +614,8 @@ bool packGame(const char *directory, const char *database) {
ok = ok && _writeMeta(writer.db, "chunk", chunk); ok = ok && _writeMeta(writer.db, "chunk", chunk);
ok = ok && _writeMeta(writer.db, "packer", VERSION_STRING); ok = ok && _writeMeta(writer.db, "packer", VERSION_STRING);
free(chunk); free(chunk);
ok = ok && _applyDirectory(&writer, root, true); ok = ok && _applyDirectory(&writer, root, !isPatch);
ok = ok && (isPatch || _validateGamesDat(&writer));
if (ok) { if (ok) {
utilSay(">>> Packed %lld files (%lld bytes) from %s into %s", (long long)writer.files, (long long)writer.bytes, root, database); utilSay(">>> Packed %lld files (%lld bytes) from %s into %s", (long long)writer.files, (long long)writer.bytes, root, database);
} }
@ -518,6 +631,26 @@ bool packGame(const char *directory, const char *database) {
} }
// A database with our meta table, whatever its extension.
bool packIsDatabase(const char *database) {
sqlite3 *db = NULL;
sqlite3_stmt *stmt = NULL;
bool found = false;
if (!utilFileExists(database) || (sqlite3_open_v2(database, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK)) {
sqlite3_close(db);
return false;
}
if (sqlite3_prepare_v2(db, "SELECT value FROM meta WHERE key = 'version'", -1, &stmt, NULL) == SQLITE_OK) {
found = (sqlite3_step(stmt) == SQLITE_ROW);
}
sqlite3_finalize(stmt);
sqlite3_close(db);
return found;
}
// Applies a patch, either a directory of replacement files or a patch database, in one transaction. // Applies a patch, either a directory of replacement files or a patch database, in one transaction.
bool packPatch(const char *database, const char *source) { bool packPatch(const char *database, const char *source) {
WriterT writer; WriterT writer;
@ -534,6 +667,7 @@ bool packPatch(const char *database, const char *source) {
utilSay("!!! %s does not exist.", source); utilSay("!!! %s does not exist.", source);
ok = false; ok = false;
} }
ok = ok && _validateGamesDat(&writer);
if (ok) { if (ok) {
utilSay(">>> Patched %s with %lld files (%lld bytes) from %s", database, (long long)writer.files, (long long)writer.bytes, source); utilSay(">>> Patched %s with %lld files (%lld bytes) from %s", database, (long long)writer.files, (long long)writer.bytes, source);
} }
@ -557,8 +691,8 @@ bool packUnpack(const char *database, const char *directory) {
int64_t files = 0; int64_t files = 0;
bool ok = true; bool ok = true;
if (!vfsIsDatabase(database)) { if (!packIsDatabase(database)) {
utilSay("!!! %s is not a Singe game database.", database); utilSay("!!! %s is not a Singe game or patch database.", database);
return false; return false;
} }
if (sqlite3_open_v2(database, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) { if (sqlite3_open_v2(database, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) {

View file

@ -10,6 +10,7 @@
char *packForbiddenReason(const char *path, bool isPatch); char *packForbiddenReason(const char *path, bool isPatch);
bool packGame(const char *directory, const char *database); bool packGame(const char *directory, const char *database);
bool packIsDatabase(const char *database);
bool packPatch(const char *database, const char *source); bool packPatch(const char *database, const char *source);
bool packUnpack(const char *database, const char *directory); bool packUnpack(const char *database, const char *directory);

View file

@ -744,7 +744,7 @@ static ConfigT *_buildConfFromTable(lua_State *L, const ConfigT *base) {
int64_t valueNumber = 0; int64_t valueNumber = 0;
ConfigT *c = NULL; ConfigT *c = NULL;
// Start with the given config, but every entry declares its own disc and container. // Start with the given config, but every entry brings its own video and container.
c = cloneConf(base); c = cloneConf(base);
free(c->container); free(c->container);
c->container = NULL; c->container = NULL;
@ -825,8 +825,6 @@ static ConfigT *_buildConfFromTable(lua_State *L, const ConfigT *base) {
c->audioDelayMs = (int32_t)valueNumber; c->audioDelayMs = (int32_t)valueNumber;
} else if (strcmp(confKey, "LEGACY_SPRITE_ARGS") == 0) { } else if (strcmp(confKey, "LEGACY_SPRITE_ARGS") == 0) {
c->legacySpriteArgs = valueBoolean; c->legacySpriteArgs = valueBoolean;
} else if (strcmp(confKey, "DISC") == 0) {
c->disc = valueBoolean;
} else if (strcmp(confKey, "CANVAS_X") == 0) { } else if (strcmp(confKey, "CANVAS_X") == 0) {
c->canvasWidth = (int32_t)valueNumber; c->canvasWidth = (int32_t)valueNumber;
} else if (strcmp(confKey, "CANVAS_Y") == 0) { } else if (strcmp(confKey, "CANVAS_Y") == 0) {
@ -837,13 +835,12 @@ static ConfigT *_buildConfFromTable(lua_State *L, const ConfigT *base) {
lua_pop(L, 1); lua_pop(L, 1);
} }
// A laserdisc game says DISC = true and names its VIDEO; anything else is a mistake worth stopping for. // The VIDEO line is the disc: present and not blank means a laserdisc game.
if (c->disc && (c->videoFile == NULL)) { if ((c->videoFile != NULL) && (c->videoFile[0] == 0)) {
utilDie("%s: DISC = true but no VIDEO given.", c->scriptFile); free(c->videoFile);
} c->videoFile = NULL;
if (!c->disc && (c->videoFile != NULL)) {
utilDie("%s: VIDEO given without DISC = true. Add DISC = true to the games.dat entry for a laserdisc game.", c->scriptFile);
} }
c->disc = (c->videoFile != NULL);
if ((c->canvasWidth <= 0) || (c->canvasHeight <= 0)) { if ((c->canvasWidth <= 0) || (c->canvasHeight <= 0)) {
utilDie("%s: CANVAS_X and CANVAS_Y must be positive.", c->scriptFile); utilDie("%s: CANVAS_X and CANVAS_Y must be positive.", c->scriptFile);
} }
@ -4445,11 +4442,6 @@ ConfigT *confFromDatabase(const ConfigT *conf) {
} }
lua_replace(L, 1); lua_replace(L, 1);
lua_settop(L, 1); lua_settop(L, 1);
// --disc on the command line stands in for a DISC = true the entry forgot.
if (conf->disc) {
lua_pushboolean(L, true);
lua_setfield(L, 1, "DISC");
}
result = _buildConfFromTable(L, base); result = _buildConfFromTable(L, base);
if (result->scriptFile == NULL) { if (result->scriptFile == NULL) {
utilDie("%s: the first games.dat entry has no SCRIPT.", base->container); utilDie("%s: the first games.dat entry has no SCRIPT.", base->container);