feat(cheats): ignore unreferenced static constant tables (#1119)

This commit is contained in:
Joe Mattiello authored and GitHub committed 2026-10-28 22:22:12 -04:00
1 parent db90f8e3ac
commit 82d18eb5d4
8 files changed
+192 -11

No files matched your search

+2 -1
View File
@@ -14,7 +14,7 @@ th,td{text-align:left;padding:.25rem .5rem;border-bottom:1px solid rgba(127,127,
</head>
<body>
<h1>Community cheats</h1>
<p>Cheat packs for 26 Atari Jaguar cartridges (137 cheats), converted from community
<p>Cheat packs for 27 Atari Jaguar cartridges (139 cheats), converted from community
scripts. The <a href="../app.html">Virtual Jaguar desktop app</a> downloads them per cartridge; every cheat starts off.
Packs are bound to the cartridge by <code>vj_rom_crc32</code>.</p>
<ul>
@@ -29,6 +29,7 @@ Packs are bound to the cartridge by <code>vj_rom_crc32</code>.</p>
<tr><td>Defender 2000</td><td>Defender 2000 (1996)</td><td><code>27594C6A</code></td><td>40</td><td>partial</td><td><a href="packs/27594C6A.cht">27594C6A.cht</a></td></tr>
<tr><td>Doom - Evil Unleashed</td><td>Doom - Evil Unleashed (1994)</td><td><code>5E2CDBC0</code></td><td>8</td><td>complete</td><td><a href="packs/5E2CDBC0.cht">5E2CDBC0.cht</a></td></tr>
<tr><td>Double Dragon V</td><td>Double Dragon V (1995) (Williams)</td><td><code>348E6449</code></td><td>1</td><td>complete</td><td><a href="packs/348E6449.cht">348E6449.cht</a></td></tr>
<tr><td>Evolution - Dino Dudes</td><td>Evolution - Dino Dudes (1993)</td><td><code>0EC5369D</code></td><td>2</td><td>partial</td><td><a href="packs/0EC5369D.cht">0EC5369D.cht</a></td></tr>
<tr><td>Hover Strike</td><td>Hover Strike (1995)</td><td><code>4899628F</code></td><td>10</td><td>complete</td><td><a href="packs/4899628F.cht">4899628F.cht</a></td></tr>
<tr><td>Iron Soldier</td><td>Iron Soldier (1994)</td><td><code>08F15576</code></td><td>3</td><td>complete</td><td><a href="packs/08F15576.cht">08F15576.cht</a></td></tr>
<tr><td>Kasumi Ninja</td><td>Kasumi Ninja (1994)</td><td><code>0957A072</code></td><td>1</td><td>complete</td><td><a href="packs/0957A072.cht">0957A072.cht</a></td></tr>
+38 -4
View File
@@ -1,14 +1,14 @@
{
"counts": {
"cheats": 137,
"packs": 26,
"partial_packs": 11,
"cheats": 139,
"packs": 27,
"partial_packs": 12,
"unsupported_entries": 156
},
"format": 1,
"generated_from": {
"cartridges_sha256": "edebcd7566e15f2426cacf5181379fbdd7b47c697110ccf79bb831205f3e853a",
"converter_sha256": "944c9c0a03c9e1b1e958f74cf07d4000ad05fd5b4f785b3b754a073203c01fd2",
"converter_sha256": "2775f4a2b9a652737893424b0131d019e823a8b2f86e2d57aa2ca985d75c3f08",
"generator_sha256": "452c26c2a6b8a38501545cfc7853c74a4d8a90cd5b75fda732f6e8020df314db",
"sources": [
{
@@ -180,6 +180,40 @@
"updated": "2026-04-15",
"writes": 1
},
{
"bytes": 830,
"cheats": 2,
"crc32": "0EC5369D",
"credit": "Cheats by jguffer (https://github.com/jguffer/BigPEmu_Scripts), converted by the Virtual Jaguar project with the author's permission.",
"exceeds_core_limit": false,
"file": "packs/0EC5369D.cht",
"fnv1a64": "8ED8554F0955F6EF",
"messages": [],
"name": "Evolution - Dino Dudes (1993)",
"notes": [
{
"path": "scripts/cheat_evolution_dino_dudes.c",
"reason": "parameter setting exceeds 64-value limit",
"setting": "Set Last Used Password Level RTFM",
"source": "jguffer"
}
],
"overlaps": [],
"partial": true,
"sha256": "4b70d79871ec39c4c26462de725d6d0509c324ceb4208df78106fcff3485627b",
"sources": [
{
"converted": 2,
"omitted": 1,
"path": "scripts/cheat_evolution_dino_dudes.c",
"sha256": "cc0cfc14bcb0a8ecc3719a7567ddbc0c9b62466bf4760d304ce32736019b2744",
"source": "jguffer"
}
],
"title": "Evolution - Dino Dudes",
"updated": "2026-04-15",
"writes": 2
},
{
"bytes": 596,
"cheats": 1,
+16
View File
@@ -0,0 +1,16 @@
# Community cheats for Evolution - Dino Dudes (1993); nothing is enabled automatically.
# Credit: Cheats by jguffer (https://github.com/jguffer/BigPEmu_Scripts), converted by the Virtual Jaguar project with the author's permission.
# Source: jguffer/BigPEmu_Scripts@24211503339a969f8494b8e189b8f2aa0cb9eba6 scripts/cheat_evolution_dino_dudes.c
# Source SHA256: cc0cfc14bcb0a8ecc3719a7567ddbc0c9b62466bf4760d304ce32736019b2744
# Omitted: "Set Last Used Password Level RTFM" (scripts/cheat_evolution_dino_dudes.c): parameter setting exceeds 64-value limit
# Cartridge revision guard is enforced by the Virtual Jaguar app.
vj_rom_crc32 = "0EC5369D"
cheats = 2
cheat0_desc = "Infinite Extra Dudes"
cheat0_code = "00047BC2:4848"
cheat0_enable = false
cheat1_desc = "Infinite Time"
cheat1_code = "00047BAE:4040"
cheat1_enable = false
+3 -3
View File
@@ -1555,11 +1555,11 @@
"crc32": "0EC5369D",
"note": null,
"path": "scripts/cheat_evolution_dino_dudes.c",
"reason": "pointer/array declarations require manual review",
"setting": null,
"reason": "parameter setting exceeds 64-value limit",
"setting": "Set Last Used Password Level RTFM",
"sha256": "cc0cfc14bcb0a8ecc3719a7567ddbc0c9b62466bf4760d304ce32736019b2744",
"source": "jguffer",
"stage": "module"
"stage": "setting"
},
{
"calls": [],
+9 -2
View File
@@ -73,8 +73,15 @@ authenticity signature. Do not remove it to make a different dump pass.
expressions for the size and entries (1024 elements total per evaluation).
An omitted size is inferred from the initializer, and missing trailing entries
are zero-filled. Indexing preserves integer types and setting dependencies;
negative and out-of-range indices are refused. Pointers, mutations, local or
multidimensional arrays, strings, and designated initializers remain unsupported.
negative and out-of-range indices are refused. Pointers, mutations, local arrays,
and reading multidimensional/string-initialized arrays remain unsupported.
- Unreferenced, non-volatile global `static const` integer tables with explicit
positive constant dimensions may be ignored after checking their declaration
and initializer for side effects, including macro expansions. This permits
inert nested reference tables and string rows in 8-bit tables. References in
any function, other global initializer, macro expansion, or unreachable branch
prevent ignoring the table. Ignored data is not materialized or zero-filled
and does not consume the 1024-element limit for tables that are actually read.
- Top-level `const` scalar integer locals in `bigp_init`, initialized from pure
integer expressions, macros, enums, globals, or earlier constant locals. Integer
setting registrations see only declarations that precede them. The locals are
+48 -1
View File
@@ -39,7 +39,7 @@ INIT_CALLS = frozenset(n for n in SPECIAL_CALLS if n.startswith(("bigpemu_regist
API_STATUSES = ("supported", "setting-omitted", "ignored", "unsupported")
MAX_MESSAGES, MAX_MESSAGE_CHARS, NOTE_CHARS = 4, 200, 60
MAX_INTEGER_VALUES, MAX_SETTING_COMBINATIONS = 64, 4096
MAX_ARRAY_ELEMENTS = 1024 # Total across all constant tables in one evaluation.
MAX_ARRAY_ELEMENTS = 1024 # Total materialized table elements per evaluation.
@functools.lru_cache(maxsize=None)
@@ -421,6 +421,9 @@ class Runtime:
raise Unsupported("unsupported statement " + type(node).__name__)
def declare_array(self, node):
if id(node) in self.module.unused_tables:
self.check_unused_table(node)
return
element = node.type.type
if (len(self.scopes) != 1 or not isinstance(element, C.TypeDecl)
or "const" not in element.quals or not isinstance(node.init, C.InitList)):
@@ -445,6 +448,35 @@ class Runtime:
self.array_elements += size.n
self.scopes[-1][node.name] = ConstantArray(tuple(values))
def check_unused_table(self, node):
# An unreferenced static object cannot contribute to callback writes.
# Validate its declaration without allocating its rows or zero-fill.
if any(isinstance(n, (C.FuncCall, C.Assignment, C.NamedInitializer)) or
(isinstance(n, C.UnaryOp) and n.op in ("++", "--", "p++", "p--"))
for n in walk(node, self.module.macros)):
raise Unsupported("unused static table must have a side-effect-free declaration")
element = node.type
while isinstance(element, C.ArrayDecl):
size = self.expr(element.dim)
if size.deps or size.n <= 0:
raise Unsupported("unused static table requires positive constant dimensions")
element = element.type
typ = type_info(element)
if not isinstance(node.init, C.InitList) and not (
isinstance(node.init, C.Constant) and node.init.type == "string"):
raise Unsupported("unused static table requires an initializer")
pending = [node.init]
while pending:
self.tick()
item = pending.pop()
if isinstance(item, C.InitList):
pending.extend(item.exprs or [])
elif isinstance(item, C.Constant) and item.type == "string":
if typ[0] != 8:
raise Unsupported("unused string table requires 8-bit integer elements")
elif integer(self.expr(item), *typ).deps:
raise Unsupported("unused static table initializer depends on runtime state")
def run(self, name):
body = self.module.functions[name].body
statements = list(body.block_items or [])
@@ -590,6 +622,21 @@ class Module:
else: enum = None
self.globals.append((node, enum))
# Count all syntactic references, including macros and code that is not
# reached by conversion. A declaration name itself is not an ID node.
references = {n.name for n in walk(tree, self.macros) if isinstance(n, C.ID)}
self.unused_tables = set()
for node, _ in self.globals:
element, dimensions = node.type, []
while isinstance(element, C.ArrayDecl):
dimensions.append(element.dim)
element = element.type
if (dimensions and all(dim is not None for dim in dimensions)
and node.storage == ["static"] and node.name not in references
and isinstance(element, C.TypeDecl) and "const" in element.quals
and "volatile" not in element.quals):
self.unused_tables.add(id(node))
self.parameter_args = parameter_args
self.init_constants, self.parameter_prefixes, self.init_handles = init_constants, parameter_prefixes, init_handles
reserved = set(self.macros) | init_handles | set(self.functions) | INIT_CALLS
+14
View File
@@ -124,6 +124,20 @@ class PackTests(unittest.TestCase):
self.assertEqual(load(files, "unsupported.json")["entries"], [])
self.assertFalse(load(files, "manifest.json")["packs"][0]["partial"])
def test_unused_static_table_pack_and_revision_guard(self):
text = source(SIMPLE, extra='static const uint8_t reference[80][14] = {"REFERENCE"};')
inputs = {"s": {"scripts/cheat_reference.c": text}}
files = make(inputs)
self.assertEqual(files, make(inputs))
names, codes, enables = parse_cht(files[f"packs/{CRC}.cht"])
self.assertEqual(codes, ["001F0000:AB", "001F0002:1234"])
self.assertEqual(enables, ["false", "false"])
self.assertIn(f'vj_rom_crc32 = "{CRC}"', files[f"packs/{CRC}.cht"].decode())
self.assertEqual(load(files, "unsupported.json")["entries"], [])
wrong_source = text + f"\n#define OTHER_CART 0x{DIGEST ^ 1:016X}ULL\n"
with self.assertRaisesRegex(bp.BuildError, "no packs were converted"):
make({"s": {"scripts/cheat_reference.c": wrong_source}}, table={f"{DIGEST ^ 1:016X}": cart(DIGEST ^ 1)})
def test_header_title_not_duplicated(self):
def first(table):
files = make({"s": {"scripts/cheat_a.c": source(SIMPLE)}}, table=table)
+62
View File
@@ -98,6 +98,68 @@ class ConversionTests(unittest.TestCase):
'bigpemu_register_setting_bool(category, "Other freeze", 0)',
'bigpemu_register_setting_int_full(category, "Password", -1, -1, 3, 1)')
def test_unused_static_const_table_does_not_block_freezes(self):
table = 'static const uint8_t reference[80][14] = {"REFERENCE", "FOR DISPLAY"};'
module = converter.Module(source(SIMPLE, extra=table))
self.assertEqual([r["code"] for r in module.convert(DIGEST)], ["001F0000:AB", "001F0002:1234"])
self.assertTrue(all("code" not in r for r in module.convert(DIGEST ^ 1)))
def test_unused_table_is_not_materialized_or_charged_to_array_budget(self):
text = source("if (a) { bigpemu_jag_write8(0x1f0000, active[0]); }", extra=
'static const uint8_t reference[80][14] = {"REFERENCE"}; '
'const uint8_t active[1024] = {9};')
self.assertEqual(converter.Module(text).convert(DIGEST)[0]["code"], "001F0000:09")
def test_table_references_in_macros_and_unreachable_code_prevent_skip(self):
table = 'static const uint8_t reference[2][4] = {"ABC", "DEF"};'
for body in ('if (0) { bigpemu_jag_write8(0x1f0000, reference[0][0]); }',
'if (a) { bigpemu_jag_write8(0x1f0000, ALIAS[0][0]); }',
'if (a) { bigpemu_jag_sysmemwrite(0x1f0000, &reference[0], 4); }'):
text = source(body + SIMPLE, extra=table + '\n#define ALIAS reference\n')
with self.subTest(body=body), self.assertRaisesRegex(converter.Unsupported, "pointer/array"):
converter.Module(text).convert(DIGEST)
text = source(SIMPLE, extra=table + '\nuint32_t helper(void) { return reference[0][0]; }')
with self.assertRaisesRegex(converter.Unsupported, "pointer/array"):
converter.Module(text).convert(DIGEST)
text = source(SIMPLE, extra=table + '\nconst int dependent = reference[0][0];')
with self.assertRaisesRegex(converter.Unsupported, "pointer/array"):
converter.Module(text).convert(DIGEST)
def test_unused_table_requires_static_const_integer_storage(self):
for table in ('const uint8_t reference[2][4] = {"ABC"};',
'static uint8_t reference[2][4] = {"ABC"};',
'static const volatile uint8_t reference[2][4] = {"ABC"};',
'static const uint8_t *reference[2] = {0};',
'static const uint8_t reference[2][4];'):
with self.subTest(table=table), self.assertRaises(converter.Unsupported):
converter.Module(source(SIMPLE, extra=table)).convert(DIGEST)
def test_unused_table_rejects_hidden_effects_and_invalid_dimensions(self):
for table in ('static const uint8_t reference[2][4] = {{BUMP}};',
'static const uint8_t reference[BUMP][4] = {{0}};',
'static const uint8_t reference[2][4] = {{bigpemu_jag_read8(0)}};',
'static const uint8_t reference[0][4] = {{0}};',
'static const uint8_t reference[-1][4] = {{0}};',
'static const uint8_t reference[missing][4] = {{0}};',
'static const uint8_t reference[2][4] = {{missing}};',
'static const int8_t reference[2][4] = {{999}};',
'static const uint8_t reference[][4] = {{0}};'):
text = source(SIMPLE, extra='int counter = 1;\n#define BUMP (++counter)\n' + table)
with self.subTest(table=table), self.assertRaises(converter.Unsupported):
converter.Module(text).convert(DIGEST)
def test_unused_table_does_not_skip_other_global_declarations(self):
text = source(SIMPLE, extra='static const uint8_t reference[2][4] = {"ABC"}; int *other;')
with self.assertRaisesRegex(converter.Unsupported, "pointer/array"):
converter.Module(text).convert(DIGEST)
def test_unused_table_name_does_not_enable_skipping_local_arrays(self):
text = source('if (a) { const uint8_t reference[2][4] = {"ABC"}; }' + SIMPLE,
extra='static const uint8_t reference[2][4] = {"DEF"};')
result = converter.Module(text).convert(DIGEST)
self.assertIn("pointer/array", result[0]["unsupported"])
self.assertIn("code", result[1])
def test_constant_table_passwords_and_sequenced_loop_update(self):
module = converter.Module(self.table_source())
results = module.convert(DIGEST)