From ed0080ab2c6bbae24e9af7d2320c8dfeb1921355 Mon Sep 17 00:00:00 2001 From: acamilo Date: Wed, 23 Sep 2026 11:06:07 +0000 Subject: [PATCH] symbols: the six WRAM names the engagement rewards read wSpriteIndex, wWalkCounter, wMapSpriteExtraData, wToggleableObjectFlags, wToggleableObjectList and wObtainedHiddenItemsFlags, resolved by tools/resolve_wram.py from ram/wram.asm at the pinned commit and bracketed by addresses symbols.rs already carries. Two of the brackets needed the tool to count NUM_STATS and NUM_CITY_MAPS, which the decomp defines as const_value over an enumeration; both are now counted up to the line that defines them. --- .../flybrain-gb/src/pokemon_red/symbols.rs | 6 +++ services/flysim/tools/gen_symbols.py | 16 +++++++ services/flysim/tools/resolve_wram.py | 45 +++++++++++++++++-- 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/services/flysim/crates/flybrain-gb/src/pokemon_red/symbols.rs b/services/flysim/crates/flybrain-gb/src/pokemon_red/symbols.rs index 014b4ad..e62c245 100644 --- a/services/flysim/crates/flybrain-gb/src/pokemon_red/symbols.rs +++ b/services/flysim/crates/flybrain-gb/src/pokemon_red/symbols.rs @@ -28,9 +28,11 @@ pub mod ram { pub const wJoyIgnore: u16 = 0xcd6b; // 52587 pub const wNumMovesMinusOne: u16 = 0xcd6c; // 52588 pub const wBattleResult: u16 = 0xcf0b; // 53003 + pub const wSpriteIndex: u16 = 0xcf13; // 53011 pub const wItemList: u16 = 0xcf7b; // 53115 pub const wListMenuID: u16 = 0xcf94; // 53140 pub const wFontLoaded: u16 = 0xcfc4; // 53188 + pub const wWalkCounter: u16 = 0xcfc5; // 53189 pub const wEnemyMonSpecies: u16 = 0xcfe5; // 53221 pub const wEnemyMonHP: u16 = 0xcfe6; // 53222 pub const wEnemyMonLevel: u16 = 0xcff3; // 53235 @@ -71,11 +73,15 @@ pub mod ram { pub const wSignCoords: u16 = 0xd4b1; // 54449 pub const wSignTextIDs: u16 = 0xd4d1; // 54481 pub const wNumSprites: u16 = 0xd4e1; // 54497 + pub const wMapSpriteExtraData: u16 = 0xd504; // 54532 pub const wTilesetBank: u16 = 0xd52b; // 54571 pub const wTilesetBlocksPtr: u16 = 0xd52c; // 54572 pub const wTilesetCollisionPtr: u16 = 0xd530; // 54576 pub const wTilesetTalkingOverTiles: u16 = 0xd532; // 54578 pub const wNumHoFTeams: u16 = 0xd5a2; // 54690 + pub const wToggleableObjectFlags: u16 = 0xd5a6; // 54694 + pub const wToggleableObjectList: u16 = 0xd5ce; // 54734 + pub const wObtainedHiddenItemsFlags: u16 = 0xd6f0; // 55024 pub const wStatusFlags5: u16 = 0xd730; // 55088 pub const wStatusFlags6: u16 = 0xd732; // 55090 pub const wStatusFlags7: u16 = 0xd733; // 55091 diff --git a/services/flysim/tools/gen_symbols.py b/services/flysim/tools/gen_symbols.py index b499e43..959ba83 100644 --- a/services/flysim/tools/gen_symbols.py +++ b/services/flysim/tools/gen_symbols.py @@ -209,6 +209,22 @@ EXTRA_RAM = ( # services/flysim/tools/resolve_wram.py is the second reading of it, from ram/wram.asm at # this commit, bracketed by wFontLoaded and wForcePlayerToChooseMon. 'wCapturedMonSpecies', + # The engagement rewards (`docs/rewards-learning.md`, the operator 2026-09-23). + # `talk`: DisplayTextID copies its argument -- the sprite slot, or a sign's text id -- + # into wSpriteIndex before it looks the text up, and the overworld's A press only + # reaches it while wWalkCounter is zero. `item`: an item ball is a sprite whose + # wMapSpriteExtraData entry is (item id, 0) -- LoadMapHeader writes that shape for + # an ITEM-flagged object_event and no other -- found in the map's + # wToggleableObjectList; PickUpItem hides it by setting its global bit in + # wToggleableObjectFlags, and FoundHiddenItemText sets the hidden item's bit in + # wObtainedHiddenItemsFlags, each only after GiveItem succeeded. + # services/flysim/tools/resolve_wram.py is the second reading of all six. + 'wSpriteIndex', + 'wWalkCounter', + 'wMapSpriteExtraData', + 'wToggleableObjectFlags', + 'wToggleableObjectList', + 'wObtainedHiddenItemsFlags', ) diff --git a/services/flysim/tools/resolve_wram.py b/services/flysim/tools/resolve_wram.py index c7973cb..1572a84 100644 --- a/services/flysim/tools/resolve_wram.py +++ b/services/flysim/tools/resolve_wram.py @@ -57,6 +57,22 @@ WANTED = { # this one caught", and the only signal that needs no second rule to tell a # catch apart from a gift, a trade or an evolution. 'wCapturedMonSpecies': 'the species a ball just caught, 0 for none', + # The engagement rewards (`docs/rewards-learning.md`, the operator 2026-09-23). + # DisplayTextID stores its argument here -- a sprite slot, or a sign's text id -- + # before it looks the text up, so it names what a conversation is with. + 'wSpriteIndex': "DisplayTextID's argument, a sprite slot or a text id", + # Non-zero for the frames of a step; the overworld only reads an A press at zero. + 'wWalkCounter': 'frames left in the step the player is taking', + # Two bytes per sprite slot: (item id, 0) for an ITEM-flagged object_event, + # (trainer class, trainer number) for a TRAINER one, zeroes otherwise. + 'wMapSpriteExtraData': 'per sprite slot: item id, or trainer class and number', + # One bit per global toggleable object; PickUpItem's HideObject sets an item + # ball's bit once GiveItem has succeeded. + 'wToggleableObjectFlags': 'global hidden bits of every toggleable object', + # The current map's (sprite slot, global toggleable index) pairs, $ff-terminated. + 'wToggleableObjectList': "this map's toggleable sprites and their global indices", + # One bit per hidden item, set by FoundHiddenItemText once GiveItem succeeded. + 'wObtainedHiddenItemsFlags': 'hidden items already found', } @@ -76,9 +92,33 @@ WANTED = { COUNTED = { 'NUM_HMS': ('constants/item_constants.asm', r'^\s*add_hm\s+\w+'), 'NUM_TMS': ('constants/item_constants.asm', r'^\s*add_tm\s+\w+'), + # The engagement rewards need two more, for the same reason: each is an + # `EQU const_value` over an enumeration, and each is the size of a declaration + # the cursor has to cross. `ds NUM_STATS` sits between wFontLoaded and + # wTrainerClass, the bracket of wWalkCounter; `flag_array NUM_CITY_MAPS` sits + # between wNumHoFTeams and wStatusFlags5, the bracket of the toggleable-object + # and hidden-item flags. Both are counted only up to the line that defines + # them, which is where `const_value` is read. + 'NUM_STATS': ( + 'constants/battle_constants.asm', r'^\s*const\s+STAT_\w+', r'^DEF NUM_STATS\b' + ), + 'NUM_CITY_MAPS': ( + 'constants/map_constants.asm', r'^\s*map_const\s+\w+', r'^DEF NUM_CITY_MAPS\b' + ), } +def count_in(root: Path, path: str, pattern: str, stop: str | None = None) -> int: + """Lines of `path` matching `pattern`, up to the first line matching `stop`.""" + text = (root / path).read_text() + if stop is not None: + match = re.search(stop, text, re.M) + if match is None: + raise SystemExit(f'{path}: no line matches {stop}') + text = text[: match.start()] + return len(re.findall(pattern, text, re.M)) + + def pinned(text: str) -> dict[str, int]: """Every address `symbols.rs` carries today, by symbol name.""" return { @@ -95,10 +135,7 @@ def constants(root: Path) -> dict[str, int]: BLOCK_WIDTH`). A name whose expression never becomes evaluable is simply left out, which kills the cursor at any declaration that uses it. """ - counted = { - name: len(re.findall(pattern, (root / path).read_text(), re.M)) - for name, (path, pattern) in COUNTED.items() - } + counted = {name: count_in(root, *spec) for name, spec in COUNTED.items()} pending: dict[str, str] = {} sources = sorted((root / 'constants').glob('*.asm')) + sorted( (root / 'constants').glob('*.inc')