mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 04:36:00 -10:00
feat(cheats): ignore unreferenced static constant tables (#1119)
This commit is contained in:
1 parent
db90f8e3ac
commit
82d18eb5d4
8 files changed
+192
-11
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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": [],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user