diff --git a/src/runner.c b/src/runner.c index 747805dd9..9175cf042 100644 --- a/src/runner.c +++ b/src/runner.c @@ -5062,7 +5062,9 @@ static bool restoreCheckpointVariables(VMContext* vm, Instance* target, const Js if (!JsonReader_isObject(object)) return false; repeat(JsonReader_objectLength(object), i) { const char* name = JsonReader_getJsonKeyByIndex(object, i); - if (name == nullptr || name[0] == '\0') return false; + // Some GameMaker VARI chunks contain an empty name for a real variable ID. + // Checkpoint v2 already writes that name as a valid empty JSON object key. + if (name == nullptr) return false; RValue value = RValue_makeUndefined(); if (!checkpointRValueFromJson(vm, JsonReader_getJsonValueByIndex(object, i), 0, remainingCells, &value)) return false; Instance_setSelfVar(target, VM_getOrAllocateVarID(vm, name), value); diff --git a/tests/test_retrom_web_host.py b/tests/test_retrom_web_host.py index cb0215cfd..b0c71452e 100755 --- a/tests/test_retrom_web_host.py +++ b/tests/test_retrom_web_host.py @@ -77,6 +77,15 @@ def test_checkpoint_v2_preserves_supported_game_data_structures(self) -> None: self.assertIn("writeUint32LE(gCheckpointBytes + 4, 2);", self.source) self.assertIn("readUint32LE(bytes + 4) != 2", self.source) + def test_checkpoint_v2_restores_empty_game_variable_names(self) -> None: + runner = (ROOT / "src/runner.c").read_text(encoding="utf-8") + start = runner.index("static bool restoreCheckpointVariables") + end = runner.index("static void clearCheckpointInstances", start) + restore = runner[start:end] + self.assertIn("name == nullptr", restore) + self.assertNotIn("name[0] == '\\0'", restore) + self.assertIn("VM_getOrAllocateVarID(vm, name)", restore) + def test_closing_a_dirty_ini_clears_the_transient_checkpoint_blocker(self) -> None: start = self.builtins.index("static RValue builtin_ini_close") end = self.builtins.index("static RValue builtin_ini_read_string", start)