1# A Link to the Past (SNES, USA v1.0) — WRAM Map for an AI Harness 2 3Sourced from cloned repositories (read-only research clones, never pushed/starred/forked): 4 5| Repo | Local path | Role | Grade | 6|---|---|---|---| 7| `snesrev/zelda3` | `~/src/github.com/snesrev/zelda3` | C reimplementation compiled to bit-exact match against the real USA ROM (SHA1-gated, see §9). `src/variables.h` maps every named WRAM cell as `g_ram+0xNN` ⇒ `$7E0000+0xNN`. | **Primary** — this is effectively "the source code of the game," not a guess. | 8| `spannerisms/usdasm` | `~/src/github.com/spannerisms/usdasm` | Full byte-exact USA disassembly by Kan/spannerisms, converted from `jpdasm`. Flat `bank_XX.asm` files. Actively maintained (last commit 2026-07-06). | **Primary** — buildable, checksum-verified against USA ROM. | 9| `JaredBrian/AsarUSALTTPDisassembly` | `~/src/github.com/JaredBrian/AsarUSALTTPDisassembly` | USA disassembly (WIP, not yet buildable) based on MathOnNapkins' dis + Kan's USDASM, with a dedicated `Other/symbols_wram.asm` giving clean full `$7Exxxx` names, plus `Logs/Zelda_3_RAM.log` and per-sprite files under `Sprites/`. Actively maintained (last commit 2026-06-25). | **Primary/cross-check** — independent naming lineage from usdasm, very useful for cross-verification and named sprite files. | 10| `spannerisms/jpdasm` | `~/src/github.com/spannerisms/jpdasm` | JP1.0 disassembly (byte-exact, highly respected). `symbols_wram.asm`. | **Secondary/cross-check** — WRAM *addresses/structure* are essentially identical to USA (same engine); some *data values* (text/room/entrance indices) differ by region. Used only to triangulate addresses, never region-specific values. | 11| `walkingeyerobot/alttp-disassembly` | `~/src/github.com/walkingeyerobot/alttp-disassembly` | MathOnNapkins' original disassembly. Stale since 2018. | **Tertiary/stale cross-check only.** | 12| datacrystal.tcrf.net | web | Wiki | **Attempted, not usable**: the DataCrystal page for this game (`/wiki/The_Legend_of_Zelda:_A_Link_to_the_Past`) has no dedicated RAM-map subpage (only `/TBL` and `/Notes`); it points out to `romhack.ing/documents/365/` ("Zelda 3 Documentation"), which is a JS-rendered SPA that returned an empty shell to both `curl` and WebFetch. Not cited below. | 13| alttp.mymm1.com ALTTPR wiki (`ALTTPR_SRAM_Map`) | web | Wiki | **Attempted, not usable**: returned HTTP 500 / TLS internal-error on both `curl` and WebFetch at fetch time (2026-09-20); web.archive.org's CDX API was also rate-limited (429) during this session. Not cited below — noted here per this estate's sourcing-honesty rule rather than silently omitted. | 14 15**Sourcing rule applied throughout:** every fact below cites a `file:line` in one of the cloned repos. Three-way agreement (zelda3 + usdasm-lineage + JaredBrian, sometimes + jpdasm) is noted explicitly where checked — these are independent naming/authorship lineages that happened to agree byte-for-byte on every address checked, which is strong triangulation. Anything not directly backed by source is flagged **UNVERIFIED**. 16 17All addresses are given as `$7Exxxx` (WRAM bank $7E). zelda3's convention `g_ram+0xNN` is numerically `0xNN` = the offset from `$7E0000`. 18 19--- 20 21## 1. Game mode / submodule ($7E0010 / $7E0011) 22 23| Address | Name (zelda3) | Cross-check name | Size | Citation | 24|---|---|---|---|---| 25| `$7E0010` | `main_module_index` | `MODE` (jpdasm, usdasm-lineage, JaredBrian) | u8 | zelda3 `src/variables.h:2`; JaredBrian `Other/symbols_wram.asm:82`; jpdasm `symbols_wram.asm:81` (three-way exact agreement) | 26| `$7E0011` | `submodule_index` | `SUBMODE` | u8 | zelda3 `src/variables.h:3`; JaredBrian `Other/symbols_wram.asm:83`; jpdasm `symbols_wram.asm:82` | 27| `$7E00B0` | `subsubmodule_index` | — | u8 | zelda3 `src/variables.h:122` | 28| `$7E010C` | `saved_module_for_menu` | — | u8 | zelda3 `src/variables.h:159` — holds the gameplay module to restore to when a dialog/menu (module 14) finishes | 29 30### Module enum (`main_module_index`, values 0–27) 31 32This is the actual dispatch table the game's main loop indexes with `main_module_index` — `kMainRouting[main_module_index]()`. Source: zelda3 `src/misc.c:151-180` (`static PlayerHandlerFunc *const kMainRouting[28] = {...}`), call site `src/misc.c:325`. 33 34| Value | Function name | Meaning | 35|---|---|---| 36| 0 | `Module00_Intro` | Title screen / attract sequence entry | 37| 1 | `Module01_FileSelect` | File-select screen | 38| 2 | `Module02_CopyFile` | Copy-file flow | 39| 3 | `Module03_KILLFile` | Delete-file flow | 40| 4 | `Module04_NameFile` | Name-entry (new file) | 41| 5 | `Module05_LoadFile` | Loading a save into gameplay state | 42| 6 | `Module_PreDungeon` | Pre-dungeon-entry transition/setup | 43| 7 | `Module07_Dungeon` | **Dungeon gameplay** | 44| 8 | `Module08_OverworldLoad` | Load an overworld area, light or dark world | 45| 9 | `Module09_Overworld` | **Overworld gameplay**, light AND dark world — the world is `$7EF3CA`, not the module | 46| 10 | `Module08_OverworldLoad` | Load a SPECIAL overworld area. usdasm names this slot `Module0A_OverworldSpecialLoad` (`bank_00.asm:99`); zelda3 reuses the function of 8 | 47| 11 | `Module09_Overworld` | **Special overworld gameplay**: the off-grid areas (Master Sword grove, Zora's domain). usdasm `Module0B_OverworldSpecial` (`bank_00.asm:100`, body `bank_02.asm:7134`); zelda3 reuses the function of 9 | 48| 12 | `Module_Unknown0` | Unused (`assert(0)` stub) — `src/misc.c:203` | 49| 13 | `Module_Unknown1` | Unused (`assert(0)` stub) — `src/misc.c:207` | 50| 14 | `Module0E_Interface` | **Dialog / menu / HUD-overlay module** — see §7 for submodules | 51| 15 | `Module0F_SpotlightClose` | Screen transition (spotlight closing) | 52| 16 | `Module10_SpotlightOpen` | Screen transition (spotlight opening) | 53| 17 | `Module11_DungeonFallingEntrance` | Falling into a dungeon entrance (pit-warp/entrance animation) | 54| 18 | `Module12_GameOver` | Death sequence | 55| 19 | `Module13_BossVictory_Pendant` | Pendant-boss victory sequence | 56| 20 | `Module14_Attract` | Attract-mode demo playback | 57| 21 | `Module15_MirrorWarpFromAga` | Agahnim mirror-warp cutscene | 58| 22 | `Module16_BossVictory_Crystal` | Crystal-boss victory sequence | 59| 23 | `Module17_SaveAndQuit` | Save-and-quit flow | 60| 24 | `Module18_GanonEmerges` | Ganon-emerges cutscene | 61| 25 | `Module19_TriforceRoom` | Ending / triforce room | 62| 26 | `Module1A_Credits` | Credits | 63| 27 | `Module1B_SpawnSelect` | Spawn-point select (used by save-file continue point selection) | 64 65**Correction, 2026-09-20.** This table first gave 10 and 11 as the DARK WORLD call sites. They are not: usdasm's dispatch table names them `Module0A_OverworldSpecialLoad` and `Module0B_OverworldSpecial`, and the dark world runs in module 9 like the light world. zelda3 points 8/10 and 9/11 at the same two functions, which is what the wrong reading was inferred from. Light or dark is `$7EF3CA` (§4). 66 67### Player-has-control vs. cutscene/transition/dialog — verified, exact rule 68 69The engine names submodule 0 of both gameplay modules literally `*_PlayerControl`: 70- Overworld: `Module09_00_PlayerControl` — zelda3 `src/overworld.c:750` 71- Dungeon: `Module07_00_PlayerControl` — zelda3 `src/dungeon.c:6594` 72 73**Rule (from source, both functions gate identically):** 74``` 75player_has_control := 76 (main_module_index == 7 or main_module_index in {9, 11}) 77 and submodule_index == 0 78 and flag_custom_spell_anim_active == 0 ($7E0112) 79 and flag_is_link_immobilized == 0 ($7E02E4) 80 and flag_block_link_menu == 0 ($7E0FFC) 81 and (main_module_index == 7 or trigger_special_entrance == 0) ($7E04C6, overworld-only gate) 82``` 83Citations: `src/overworld.c:750-758` (the `if (!(flag_custom_spell_anim_active | flag_is_link_immobilized | flag_block_link_menu | trigger_special_entrance))` guard); `src/dungeon.c:6594-6602` (identical guard minus `trigger_special_entrance`); addresses from `src/variables.h:163` (`flag_custom_spell_anim_active`), `:246` (`flag_is_link_immobilized`), `:507` (`trigger_special_entrance`), `:791` (`flag_block_link_menu`). 84 85Any other `(main_module_index, submodule_index)` combination — including `main_module_index==14` (dialog/menu, §7), any nonzero submodule of 7/9/11 (screen scroll, warp, cutscene sub-states), or modules 0-6/12-27 — means the player does **not** have free movement control. This is a precise, code-derived rule, not a heuristic. 86 87--- 88 89## 2. Link — position, direction, state machine, combat state 90 91All addresses zelda3 `src/variables.h:5-95` unless noted. Cross-checked against JaredBrian `Other/symbols_wram.asm` and jpdasm `symbols_wram.asm` where marked "3-way". 92 93| Address | Name | Size/type | Meaning | Citation | 94|---|---|---|---|---| 95| `$7E0020` | `link_y_coord` | u16 | Y position (pixel, world-relative — same convention indoors and outdoors: absolute pixel coordinate within the current overworld/dungeon coordinate space) | zelda3 `variables.h:19`; 3-way match: JaredBrian `symbols_wram.asm:125` (`POSY`), jpdasm `symbols_wram.asm:125` | 96| `$7E0022` | `link_x_coord` | u16 | X position (pixel) | zelda3 `variables.h:20`; 3-way: JaredBrian `:127` (`POSX`), jpdasm `:127` | 97| `$7E0024` | `link_z_coord` | u16 (signed use) | Height/altitude above ground (jump arc height), **not** dungeon floor number — 0 = grounded, nonzero while airborne/jumping off a ledge; `0xffff`/`0xff00`-ish sentinel used to mean "not applicable" (overworld) | zelda3 `variables.h:21`; usage `src/overworld.c:1825,1971` (`BYTE(link_z_coord) = 0xff`), `src/player_oam.c:941` | 98| `$7E00EE` | `link_is_on_lower_level` | u8 (bool) | **This is the "floor/layer" flag** for two-story dungeon rooms (BG2 lower layer vs BG1 upper layer) — 0 = upper/normal layer, nonzero = lower layer (basement-style layer within the same room) | zelda3 `variables.h:141` | 99| `$7E0476` | `link_is_on_lower_level_mirror` | u8 | Mirror/cached copy of the above, read by sprite code | zelda3 `variables.h:463` | 100| `$7E002F` | `link_direction_facing` | u8 | Facing direction. Convention (from usage, 2-bit-per-axis style values used elsewhere as 0/2/4/6 for down/up/left/right in many ALTTP builds) — zelda3 does not spell out named enum constants for this field; **treat numeric mapping as needing a short empirical check** (log the byte while manually facing each of the 4 directions) rather than trusting a hardcoded guess | zelda3 `variables.h:29`; 3-way: JaredBrian `:170` (`DIR`), jpdasm same offset — **UNVERIFIED: exact value↔direction mapping**, only the address is triple-confirmed | 101| `$7E0066` | `link_last_direction_moved_towards` | u8 | Last direction actually moved | zelda3 `variables.h:87` | 102| `$7E0067` | `link_direction` | u8 | Current movement direction (distinct from facing) | zelda3 `variables.h:88` | 103| `$7E0026` | `link_direction_last` | u8 | Previous facing/direction snapshot | zelda3 `variables.h:22` | 104| `$7E0027` | `link_actual_vel_y` | i8 | Actual Y velocity | zelda3 `variables.h:23` | 105| `$7E0028` | `link_actual_vel_x` | i8 | Actual X velocity | zelda3 `variables.h:24` | 106| `$7E0030` | `link_y_vel` | u8 | Y velocity (intended/nominal) | zelda3 `variables.h:31` | 107| `$7E0031` | `link_x_vel` | u8 | X velocity (intended/nominal) | zelda3 `variables.h:32` | 108| `$7E005D` | `link_player_handler_state` | u8, enum | **Link's top-level state machine** — see enum table below | zelda3 `variables.h:71`; 3-way: JaredBrian `:346` (`LINKDO`), jpdasm same | 109| `$7E005E` | `link_speed_setting` | u8 | Speed tier setting | zelda3 `variables.h:72` | 110| `$7E0057` | `link_speed_modifier` | u8 | Speed modifier (e.g. Pegasus Boots dash multiplier) | zelda3 `variables.h:63` | 111| `$7E0056` | `link_is_bunny` | u8 (bool) | 1 = currently a bunny (Moon Pearl missing, in dark world) | zelda3 `variables.h:62` | 112| `$7E0055` | `link_cape_mode` | u8 | Cape-active state | zelda3 `variables.h:61` | 113| `$7E0046` | `link_incapacitated_timer` | u8 | Frames Link is incapacitated (knocked back / stunned) | zelda3 `variables.h:50` | 114| `$7E031F` | `countdown_for_blink` | u8 | **Post-damage invincibility-with-blink timer**, counts down to 0 | zelda3 `variables.h:291`; usage confirms it gates damage alongside `link_disable_sprite_damage`: `src/sprite.c:2725`, `src/sprite_main.c:1498,3846,15769` | 115| `$7E037B` | `link_disable_sprite_damage` | u8 (bool, occasionally incremented as a counter) | Boolean/refcount flag suppressing all sprite→Link damage — set during many cutscene/animation states (item-get, spin attack windup, hookshot, warps, etc.), **not itself a fixed-length invincibility timer** the way `countdown_for_blink` is | zelda3 `variables.h:351`; extremely widely used, e.g. `src/player.c:221,364,556,587,1516` (`link_disable_sprite_damage++`) | 116| `$7E0048` | `bitmask_of_dragstate` | u8, bitfield | Dash/drag state bitmask | zelda3 `variables.h:52` | 117| `$7E004D` | `link_auxiliary_state` | u8 | Auxiliary state (used with visibility/pose fields) | zelda3 `variables.h:56` | 118| `$7E004B` | `link_visibility_status` | u8 | Visibility status (e.g. hidden during warps) | zelda3 `variables.h:54` | 119| `$7E02DA` | `link_pose_for_item` | u8 | Item-use pose flag | zelda3 `variables.h:238` | 120| `$7E0301` | `link_item_in_hand` | u8 | Currently-wielded/active-use item id (boomerang in flight etc., transient — not the SRAM "equipped" slot) | zelda3 `variables.h:267` | 121| `$7E0303` | `current_item_y` | u8 | **The item currently mapped to the Y button** ("equipped item" for the harness's purposes) | zelda3 `variables.h:269`; set from `Hud_LookupInventoryItem(hud_cur_item)` at `src/hud.c:701` | 122| `$7E0202` | `hud_cur_item` | u8 | Cursor index into the inventory grid (used when choosing a new Y item) | zelda3 `variables.h:189` | 123| `$7E003C` | `button_b_frames` | u8 | Frames B has been held — the sword-charge counter (spin attack becomes available once this exceeds a threshold) | zelda3 `variables.h:40`; gating usage `src/player_oam.c:1178` (`... || button_b_frames >= 9`) | 124| `$7E003D` | `link_delay_timer_spin_attack` | u8 | Spin-attack delay timer | zelda3 `variables.h:41` | 125| `$7E0079` | `link_spin_attack_step_counter` | u8 | Spin-attack animation step counter | zelda3 `variables.h:91` | 126| `$7E031C` | `state_for_spin_attack` | u8 | Spin-attack sub-state | zelda3 `variables.h:288` | 127| `$7E031D` | `step_counter_for_spin_attack` | u8 | (second) spin-attack step counter | zelda3 `variables.h:289` | 128| `$7E0314` | `flag_is_sprite_to_pick_up` | u8 (bool) | Set when standing over a liftable sprite (pot/rock/sign/bush) — "carrying" state gate | zelda3 `variables.h:282` | 129| `$7E02F4` | `flag_is_sprite_to_pick_up_cached` | u8 | Cached copy of the above | zelda3 `variables.h:260` | 130| `$7E0359` | `link_sword_type` | u8 | 0 = no sword; conventionally 1=Fighter, 2=Master (enables sword-beam-at-full-health, gated at `src/sprite_main.c:6667`), 3=Tempered, 4=Golden (L4) — only 0/1/2/0xff are directly branch-tested in zelda3's C source | zelda3 `variables.h:1088` (as `$7EF359`, see §3 — same field, listed here for combat-state relevance); tests: `src/player_oam.c:739,1178`, `src/sprite_main.c:6667,6692` | 131| `$7E037A` | `link_position_mode` | u8 | Position-lock mode (e.g. during boomerang throw) | zelda3 `variables.h:350` | 132 133### Link state machine enum (`link_player_handler_state`, $7E005D) 134 135Complete enum from zelda3 `src/player.h:7-34`. Gaps (11, 13, 15, 16, 24, 31, 32...) are states with no named C constant in this header — either truly unused values or states the reimplementation didn't need to name; treat unlisted values as **unknown/needs field verification**, not confirmed-unused. 136 137| Value | Name | Meaning | 138|---|---|---| 139| 0 | `kPlayerState_Ground` | Normal ground movement | 140| 1 | `kPlayerState_FallingIntoHole` | Falling into a pit | 141| 2 | `kPlayerState_RecoilWall` | Recoiling off a wall | 142| 3 | `kPlayerState_SpinAttacking` | Spin attack in progress | 143| 4 | `kPlayerState_Swimming` | **Swimming** | 144| 5 | `kPlayerState_TurtleRock` | Turtle Rock ice-physics state | 145| 6 | `kPlayerState_RecoilOther` | Recoiling (other cause) | 146| 7 | `kPlayerState_Electrocution` | Being electrocuted | 147| 8 | `kPlayerState_Ether` | Casting Ether medallion | 148| 9 | `kPlayerState_Bombos` | Casting Bombos medallion | 149| 10 | `kPlayerState_Quake` | Casting Quake medallion | 150| 12 | `kPlayerState_FallOfLeftRightLedge` | Falling off a side ledge | 151| 14 | `kPlayerState_JumpOffLedgeDiag` | Jumping off a diagonal ledge | 152| 17 | `kPlayerState_StartDash` | **Dash starting** (Pegasus Boots) | 153| 18 | `kPlayerState_StopDash` | Dash stopping | 154| 19 | `kPlayerState_Hookshot` | Hookshot travel | 155| 20 | `kPlayerState_Mirror` | Using the Magic Mirror | 156| 21 | `kPlayerState_HoldUpItem` | **Holding up an item (item-get pose)** — also the general "lifted object overhead" pose | 157| 22 | `kPlayerState_AsleepInBed` | Asleep in bed (game start) | 158| 23 | `kPlayerState_PermaBunny` | Permanently a bunny (no Moon Pearl, in a bunny-forcing area) | 159| 25 | `kPlayerState_ReceivingEther` | Receiving the Ether medallion cutscene | 160| 26 | `kPlayerState_ReceivingBombos` | Receiving the Bombos medallion cutscene | 161| 27 | `kPlayerState_OpeningDesertPalace` | Desert Palace opening-prayer cutscene | 162| 28 | `kPlayerState_TempBunny` | Temporarily a bunny | 163| 29 | `kPlayerState_PullForRupees` | Pulling a "grab for rupees" object | 164| 30 | `kPlayerState_SpinAttackMotion` | Spin attack motion (distinct from state 3) | 165 166"Dashing" ⇒ state ∈ {17, 18}. "Swimming" ⇒ state == 4. "Falling" ⇒ state ∈ {1, 12, 14}. "Carrying/lifting" ⇒ state == 21 **and** `flag_is_sprite_to_pick_up` set, or inferred from `link_item_in_hand`/pose fields — this composite is not a single field; state 21 alone is shared by item-get poses too, so distinguish via context (module/submodule + which item fields are nonzero). 167 168--- 169 170## 3. Health, magic, currency, and the full inventory block ($7EF340..) 171 172**Important structural note, confirmed from source:** this `$7EF3xx` block is WRAM that mirrors what gets written to SRAM on save — zelda3 names every field with a `link_` prefix directly in `variables.h` at these live addresses (not a separate "SRAM shadow" struct); i.e. reading `$7EF36D` from a live emulator gives you Link's current HP right now, same address space, no indirection needed. 173 174| Address | Name | Size | Meaning | Citation | 175|---|---|---|---|---| 176| `$7EF340` | `link_item_bow` | u8 | Bow: 0=none, conventionally 2=Bow, 3=Silver Bow (only presence/absence, `!=0`, is directly branch-tested in zelda3 — exact tier values **not confirmed from source**, see `src/hud.c:1228` `if (item==1 && link_item_bow != 1)`) | zelda3 `variables.h:1064` | 177| `$7EF341` | `link_item_boomerang` | u8 | 0=none, 1=Blue, 2=Red (conventional; only nonzero-ness confirmed in source) | zelda3 `variables.h:1065` | 178| `$7EF342` | `link_item_hookshot` | u8 (bool) | Hookshot owned | zelda3 `variables.h:1066` | 179| `$7EF343` | `link_item_bombs` | u8 | **This is the live bomb count**, not a boolean (bombs have no separate owned-flag — count itself gates "have bombs") | zelda3 `variables.h:1067` | 180| `$7EF344` | `link_item_mushroom` | u8 | Mushroom/Powder item state | zelda3 `variables.h:1068` | 181| `$7EF345` | `link_item_fire_rod` | u8 (bool) | Fire Rod owned | zelda3 `variables.h:1069` | 182| `$7EF346` | `link_item_ice_rod` | u8 (bool) | Ice Rod owned | zelda3 `variables.h:1070` | 183| `$7EF347` | `link_item_bombos_medallion` | u8 (bool) | Bombos owned | zelda3 `variables.h:1071` | 184| `$7EF348` | `link_item_ether_medallion` | u8 (bool) | Ether owned | zelda3 `variables.h:1072` | 185| `$7EF349` | `link_item_quake_medallion` | u8 (bool) | Quake owned | zelda3 `variables.h:1073` | 186| `$7EF34A` | `link_item_torch` | u8 (bool) | Fire rod's "lit torch" carry state / lantern-lit flag | zelda3 `variables.h:1074` | 187| `$7EF34B` | `link_item_hammer` | u8 (bool) | Hammer owned | zelda3 `variables.h:1075` | 188| `$7EF34C` | `link_item_flute` | u8 | 0=none, 1=shovel(?), 2=flute (conventional tiers; nonzero-ness only is what source branches on) | zelda3 `variables.h:1076` | 189| `$7EF34D` | `link_item_bug_net` | u8 (bool) | Bug net owned | zelda3 `variables.h:1077` | 190| `$7EF34E` | `link_item_book_of_mudora` | u8 (bool) | Book of Mudora owned | zelda3 `variables.h:1078` | 191| `$7EF34F` | `link_item_bottle_index` | u8 | Index/count related to bottle slots | zelda3 `variables.h:1079` | 192| `$7EF350` | `link_item_cane_somaria` | u8 (bool) | Cane of Somaria owned | zelda3 `variables.h:1080` | 193| `$7EF351` | `link_item_cane_byrna` | u8 (bool) | Cane of Byrna owned | zelda3 `variables.h:1081` | 194| `$7EF352` | `link_item_cape` | u8 (bool) | Magic Cape owned | zelda3 `variables.h:1082` | 195| `$7EF353` | `link_item_mirror` | u8 (bool) | Magic Mirror owned | zelda3 `variables.h:1083` | 196| `$7EF354` | `link_item_gloves` | u8 | 0=none, 1=Power Glove, 2=Titan's Mitt (conventional; only presence directly confirmed) | zelda3 `variables.h:1084` | 197| `$7EF355` | `link_item_boots` | u8 (bool) | Pegasus Boots owned | zelda3 `variables.h:1085` | 198| `$7EF356` | `link_item_flippers` | u8 (bool) | Flippers owned | zelda3 `variables.h:1086` | 199| `$7EF357` | `link_item_moon_pearl` | u8 (bool) | Moon Pearl owned — directly gates bunny transformation, e.g. `src/messaging.c:288,320` | zelda3 `variables.h:1087` | 200| `$7EF359` | `link_sword_type` | u8 | See §2 combat table | zelda3 `variables.h:1088` | 201| `$7EF35A` | `link_shield_type` | u8 | 0=none; `==3` directly tested as the Mirror-Shield-reflects condition (`src/hud.c:261`, `src/sprite_main.c:20911,21252`); conventionally 1=Fighter, 2=Fire/Red, 3=Mirror | zelda3 `variables.h:1089` | 202| `$7EF35B` | `link_armor` | u8 | Tunic/armor level (0=Green,1=Blue,2=Red conventional) | zelda3 `variables.h:1090` | 203| `$7EF35C`-`$7EF35F` | `link_bottle_info[0..3]` | u8×4 | Per-bottle contents. Values confirmed from source: assignment pattern is "empty→2" then "2→(j+3)" (`src/misc.c:852-861`); `==6` is checked for a specific potion type at `src/messaging.c:696,2917` and `src/sprite_main.c:24058` checks `==2`. Conventional community mapping (not independently re-derived here): 0=no bottle,1=empty,2=Red Potion,3=Green Potion,4=Blue Potion,5=Fairy,6=Bee,7=Good Bee — **treat 3/4/5/7 as unverified-by-this-session, only 0/1/2/6 have a direct source citation** | zelda3 `variables.h:1091` | 204| `$7EF360` | `link_rupees_goal` | u16 | Rupee counter's *target* value (counts up/down toward `link_rupees_actual` for the HUD roll animation) | zelda3 `variables.h:1092` | 205| `$7EF362` | `link_rupees_actual` | u16 | **The real spendable rupee count** | zelda3 `variables.h:1093` | 206| `$7EF364` | `link_compass` | u16, bitfield | Compass-owned bitfield (bit per dungeon) — layout below | zelda3 `variables.h:1094` | 207| `$7EF366` | `link_bigkey` | u16, bitfield | Big-key-owned bitfield, **identical bit layout** to compass | zelda3 `variables.h:1095` | 208| `$7EF368` | `link_dungeon_map` | u16, bitfield | Dungeon-map-owned bitfield, **identical bit layout** to compass | zelda3 `variables.h:1096` | 209| `$7EF36A` | `link_rupees_in_pond` | u8 | Wishing-pond rupee toss counter | zelda3 `variables.h:1097` | 210| `$7EF36B` | `link_heart_pieces` | u8 | Heart-piece count, 0-3 (4th piece rolls into capacity) | zelda3 `variables.h:1098` | 211| `$7EF36C` | `link_health_capacity` | u8 | **Max HP, in 1/8-heart units** (`>>3` = heart count) — confirmed unit: `src/hud.c:424` `kMaxHealthForLevel[link_health_capacity >> 3]`, `src/messaging.c:794` | zelda3 `variables.h:1099` | 212| `$7EF36D` | `link_health_current` | u8 | **Current HP, in 1/8-heart units** | zelda3 `variables.h:1100`; 3-way address match not directly grepped but block is contiguous & internally consistent | 213| `$7EF36E` | `link_magic_power` | u8 | Current magic, out of a capacity (base 0x80, halves consumption with 1/2 Magic upgrade) — granularity confirmed 1/8-units via HUD tile math `src/hud.c:1398` (`(link_magic_power+7)>>3`) | zelda3 `variables.h:1101` | 214| `$7EF36F` | `link_num_keys` | u8 | **Current dungeon's small-key count** | zelda3 `variables.h:1102` | 215| `$7EF370` | `link_bomb_upgrades` | u8 | Bomb-capacity upgrade count | zelda3 `variables.h:1103` | 216| `$7EF371` | `link_arrow_upgrades` | u8 | Arrow-capacity upgrade count | zelda3 `variables.h:1104` | 217| `$7EF372` | `link_hearts_filler` | u8 | Sub-heart HP fill accumulator (animates HP regen) | zelda3 `variables.h:1105` | 218| `$7EF373` | `link_magic_filler` | u8 | Sub-unit magic fill accumulator | zelda3 `variables.h:1106` | 219| `$7EF374` | `link_which_pendants` | u8, bitfield | **Pendant bitfield.** zelda3 confirms bit *values* {1,2,4} via `kPendantBitMask[3] = {4, 1, 2}` (`src/messaging.c:156`) but not their name order from that call site alone. **Bit→name resolved by jpdasm's explicit documentation** (independent primary source, states the mapping directly rather than requiring index-order inference): bits `... .gbr` → bit `0x01`=`r`=Wisdom(red), `0x02`=`b`=Power(blue), `0x04`=`g`=Courage(green) (`jpdasm/symbols_sram.asm:779-781`). The two sources' bit *values* agree ({1,2,4} both places); jpdasm additionally supplies the name for each value, which zelda3's call sites alone did not settle | zelda3 `variables.h:1107`; `src/messaging.c:156,1513-1514`; jpdasm `symbols_sram.asm:779-781` | 220| `$7EF375` | `link_bomb_filler` | u8 | Sub-unit bomb fill accumulator | zelda3 `variables.h:1108` | 221| `$7EF376` | `link_arrow_filler` | u8 | Sub-unit arrow fill accumulator | zelda3 `variables.h:1109` | 222| `$7EF377` | `link_num_arrows` | u8 | **Current arrow count** | zelda3 `variables.h:1110` | 223| `$7EF379` | `link_ability_flags` | u8, bitfield | **Ability-gate bitfield, fully resolved via jpdasm's bit-comment block** (`symbols_sram.asm:795-806`, `; lrtu pbsh`, MSB→LSB): bit `0x80`=Lift, `0x40`=Read (Book of Mudora), `0x20`=Talk, `0x10`=unused-but-set-by-default, `0x08`=Pull, `0x04`=Run (Pegasus Boots), `0x02`=Swim, `0x01`=Pray (unused, HUD-clipped). Cross-checked against zelda3's own gating: `kAbilityBitmasks[]={0xE0,0x40,4,0xE0,0xE0,0xE0,0xE0,0xE0}` (`src/player.c:2100`) — mask `0x40` for action 1 = Read alone, mask `4` for action 2 = Run alone, mask `0xE0`=Lift\|Read\|Talk combined for the remaining actions — internally consistent with jpdasm's bit assignment | zelda3 `variables.h:1111`, `src/player.c:2100-2102`; jpdasm `symbols_sram.asm:795-806` | 224| `$7EF37A` | `link_has_crystals` | u8, bitfield | **Crystal bitfield, 7 dungeons — fully resolved and cross-verified between two independent primary sources.** jpdasm's bit-comment block (`symbols_sram.asm:809-817`, `; .wbs tipm`, MSB→LSB, bit7 unused): `0x40`=Skull Woods, `0x20`=Thieves' Town, `0x10`=Swamp Palace, `0x08`=Turtle Rock, `0x04`=Ice Palace, `0x02`=Palace of Darkness, `0x01`=Misery Mire. zelda3's `kCrystalBitMask[7] = {2,0x40,8,0x20,1,4,0x10}` (`src/messaging.c:157`, indexed 0-6 by overworld-map crystal-icon slot) decodes against those same bit values to: icon slot 0=Palace of Darkness(0x02), 1=Skull Woods(0x40), 2=Turtle Rock(0x08), 3=Thieves' Town(0x20), 4=Misery Mire(0x01), 5=Ice Palace(0x04), 6=Swamp Palace(0x10) — every value in zelda3's array matches a named bit in jpdasm's comment exactly, which is strong independent confirmation of both the bit-to-dungeon mapping and the map-icon-slot order | zelda3 `variables.h:1112`; `src/messaging.c:157,1517-1518`; jpdasm `symbols_sram.asm:809-817` | 225| `$7EF37B` | `link_magic_consumption` | u8 | Magic-drain-rate modifier (e.g. halved by 1/2 Magic upgrade) | zelda3 `variables.h:1113` | 226| `$7EF37C`.. | `link_keys_earned_per_dungeon[]` | u8 array | Per-dungeon key-earned counters (array, not indexed range confirmed beyond base) | zelda3 `variables.h:1114` | 227| `$7EF3C5` | `sram_progress_indicator` | u8 | **Progress/"game state" indicator, fully enumerated by jpdasm** (`GAMESTATE`, `symbols_sram.asm:848-851`): **0**=very start (progress can't yet be saved), **1**=Uncle reached, **2**=Zelda rescued, **3**=Agahnim defeated. Independently cross-checked in zelda3's own control flow: `sram_progress_indicator = 3` is set exactly at the boss-exit path that also flips `savegame_is_darkworld` — the Agahnim-defeat/dark-world-unlock transition (`src/dungeon.c:2503-2508`, `dung_savegame_state_bits|=0x8000` two lines above it is the *same* boss-kill event as §6.1's bit 11); dozens of `<2`/`>=2`/`<3` guards throughout `overworld.c`/`messaging.c`/`hud.c` (e.g. `src/overworld.c:732` weather gate) are consistent with 0-1=pre-rescue, 2=post-rescue-pre-Agahnim, 3=post-Agahnim. **This is the recommended state-machine signal for intro-sequence automation** (see §8) — no room-ID table needed | zelda3 `variables.h:1115`; jpdasm `symbols_sram.asm:848-851`; `src/dungeon.c:2503-2508` | 228| `$7EF3C6` | `sram_progress_flags` | u8, bitfield | **Resolved via jpdasm** (`PROGLITE`, `symbols_sram.asm:853-861`, bits `.fbh .zsu` as two nibbles MSB→LSB — bits 7,3 unused): `0x40`=fortune-teller-flip(`f`), `0x20`=Book-of-Mudora-mentioned(`b`, controls Aginah dialog), `0x10`=Uncle-has-left-Link's-house(`h`; 0=still asleep/present,1=gone), `0x04`=Zelda-brought-to-Sanctuary(`z`), `0x02`=Priest-visited-after-2nd-kidnap(`s`), `0x01`=**Uncle-visited-in-secret-passage**(`u`; 0=still there to find,1=already found/dead). The `0x10`/`0x01` bits are the two cleanest intro-sequence checkpoints available anywhere in this document | zelda3 `variables.h:1116`; jpdasm `symbols_sram.asm:853-861` | 229| `$7EF3C7` | `savegame_map_icons_indicator` | u8 | Controls which map icon set is drawn — `==3` pendants, `==7` crystals, `==6` set after Agahnim defeat (`src/messaging.c:1514,1518`, `src/misc.c:321`); **full 0-8 enumeration per jpdasm** (`MAPICON`, `symbols_sram.asm:863-873`): 0=red-X-on-castle(save Zelda), 1=red-X-on-Kakariko(talk to villagers), 2=red-X-on-Eastern(talk to Sahasrahla), 3=pendants-and-master-sword(obtain it), 4=master-sword-on-LW(grab it), 5=skull-on-castle(kill Agahnim), 6=crystal-on-PoD(get first crystal), 7=crystals(get all 7), 8=skull-on-GT(climb Ganon's Tower) | zelda3 `variables.h:1117`; jpdasm `symbols_sram.asm:863-873` | 230| `$7EF3C8` | `which_starting_point` | u8 | **Save file's respawn/starting point, fully enumerated by jpdasm** (`SPAWNPT`, `symbols_sram.asm:875-882`): 0=Link's house, 1=Sanctuary, 2=Prison, 3=Uncle, 4=Throne, 5=Old man cave, 6=Old man home. A coarse "which named place" signal usable without any room-ID table | zelda3 `variables.h:1118`; jpdasm `symbols_sram.asm:875-882` | 231| `$7EF3C9` | `sram_progress_indicator_3` | u8, bitfield | **Resolved via jpdasm** (`PROGLITE2`, `symbols_sram.asm:884-892`, bits `t.dp s.bh` MSB→LSB, bits 6/2 unused): `0x80`=smiths-currently-tempering-sword, `0x20`=swordsmith-rescued, `0x10`=purple-chest-opened, `0x08`=stumpy-stumped, `0x02`=bottle-purchased-from-vendor, `0x01`=bottle-received-from-hobo | zelda3 `variables.h:1119`; jpdasm `symbols_sram.asm:884-892` | 232| `$7EF3CA` | `savegame_is_darkworld` | u8 (bool) | **World flag: 0 = Light World, nonzero = Dark World** | zelda3 `variables.h:1120` | 233| `$7EF3CC` | `follower_indicator` | u8 | Which NPC "tagalong" follower Link currently has (e.g. the value `10` is checked at `src/sprite_main.c:1498`) | zelda3 `variables.h:1121` | 234| `$7EF3CD`/`$7EF3CF` | `saved_tagalong_y` / `saved_tagalong_x` | u16 each | Saved follower position | zelda3 `variables.h:1122-1123` | 235| `$7EF3D1` | `saved_tagalong_indoors` | u8 | Follower's saved indoors flag | zelda3 `variables.h:1124` | 236| `$7EF3D2` | `saved_tagalong_floor` | u8 | Follower's saved floor/layer | zelda3 `variables.h:1125` | 237| `$7EF3D3` | `follower_dropped` | u8 (bool) | Whether the follower was dropped | zelda3 `variables.h:1126` | 238| `$7EF3E7` | `deaths_per_palace` | u16 array | Death counter per palace | zelda3 `variables.h:1127` | 239| `$7EF33F` | `byte_7EF33F` | u8 | Unnamed byte immediately before the item block — likely padding/unused | zelda3 `variables.h:1063` | 240| `$7EF300` | `savegame_has_master_sword_flags` | u16 | Master-Sword-related flags | zelda3 `variables.h:1062` | 241| `$7EF280` | `save_ow_event_info` | u8 array | Overworld one-time-event flags array (e.g. rain-stopped flag at index `0x70`, `src/overworld.c:732`; Agahnim-defeated flag bit at index `0x1b`, `src/misc.c:313`) | zelda3 `variables.h:1061` | 242 243**Compass/Big-Key/Dungeon-Map bit layout** (all three share one bit order; each is 2 bytes, low byte = "SET 1", high byte = "SET 2"). Per jpdasm's explicit documentation (`jpdasm/symbols_sram.asm:719-736`, comment above `COMPASS1`/`COMPASS2`): 244``` 245 SET 2 SET 1 246xced aspm wihb tg.. 247``` 248- SET 1 (low byte, e.g. `$7EF364` for compass): `0x80`=SkullWoods(`w`), `0x40`=IcePalace(`i`), `0x20`=TowerOfHera(`h`), `0x10`=ThievesTown(`b`), `0x08`=TurtleRock(`t`), `0x04`=Ganon'sTower(`g`); low 2 bits unused 249- SET 2 (high byte, e.g. `$7EF365` for compass): `0x80`=Sewers(`x`), `0x40`=HyruleCastle(`c`), `0x20`=EasternPalace(`e`), `0x10`=DesertPalace(`d`), `0x08`=Agahnim'sTower(`a`), `0x04`=SwampPalace(`s`), `0x02`=PalaceOfDarkness(`p`), `0x01`=MiseryMire(`m`) 250 251Applies identically to `link_bigkey` (`$7EF366`/`367`) and `link_dungeon_map` (`$7EF368`/`369`). Cite: `jpdasm/symbols_sram.asm:719-736`. 252 253**Bombs vs. arrows asymmetry (confirmed):** bombs have no separate "owned" boolean — `link_item_bombs` at `$7EF343` IS the live count (in the same slot layout as boolean-item flags 340-357). Arrows are split: bow-owned is `$7EF340`, but the actual arrow count lives separately at `$7EF377` (`link_num_arrows`), outside the contiguous item-flag block. This is a real asymmetry in the data model, not a simplification — a harness reading "does Link have arrows to shoot" must check `$7EF377 > 0`, not `$7EF340`. 254 255--- 256 257## 4. Location 258 259| Address | Name | Size | Meaning | Citation | 260|---|---|---|---|---| 261| `$7E001B` | `player_is_indoors` | u8 (bool) | **Indoors flag**: 0 = overworld, nonzero = indoors (house/dungeon/cave) | zelda3 `variables.h:14`; 3-way: JaredBrian `:115` (`INDOORS`), jpdasm same | 262| `$7E008A` | `overworld_screen_index` | u16 (low byte is the real 0-127/0x00-0x7F area id; high byte largely unused on this axis) | **Overworld area index** | zelda3 `variables.h:97`; 3-way: JaredBrian `:497` (`OWSCR`), jpdasm `:492-493` (`OWSCR`/`OWSCRH`) | 263| `$7E00A0` | `dungeon_room_index` | u16 | **Dungeon/indoor room id** | zelda3 `variables.h:112`; 3-way: JaredBrian `:544` (`ROOM`), jpdasm `:539` | 264| `$7E00A2` | `dungeon_room_index_prev` | u16 | Previous room id (for detecting room transitions) | zelda3 `variables.h:113` | 265| `$7E048E` | `dungeon_room_index2` | u16 | A second/cached room-index copy | zelda3 `variables.h:475` | 266| `$7E040C` | `cur_palace_index_x2` | u16 | **Dungeon/palace id — stored pre-multiplied by 2** (used directly as a byte-table index elsewhere, e.g. `GetDungmapFloorLayout()` does `cur_palace_index_x2 >> 1` — `src/messaging.c:228`). A harness should read the raw u16 and divide by 2 to get the 0-based dungeon index. `0xFF` (raw) is used as a "no dungeon" sentinel, e.g. `src/dungeon.c:6603` (`(uint8)cur_palace_index_x2 != 0xff`) | zelda3 `variables.h:405`; 3-way: JaredBrian `:1989` (`DUNGEON`), jpdasm `:2007-2008` (`DUNGEON`/`DUNGEONH`) | 267| `$7EF3CA` | `savegame_is_darkworld` | u8 (bool) | **World**: 0 = Light World, nonzero = Dark World (see §3) | zelda3 `variables.h:1120` | 268| `$7E010E` | `which_entrance` | u8 | **Entrance id** used when transitioning between overworld and an indoor area | zelda3 `variables.h:160`; same physical address is reused as `attract_room_index` during the attract-mode module (`variables.h:1276`) — only meaningful when not in attract mode | 269| `$7E0618`/`$7E061A` | `camera_y_coord_scroll_low`/`_hi` | u16 each | Camera Y scroll position | zelda3 `variables.h:531-532` | 270| `$7E061C`/`$7E061E` | `camera_x_coord_scroll_low`/`_hi` | u16 each | Camera X scroll position | zelda3 `variables.h:533-534` | 271| `$7E00EE` | `link_is_on_lower_level` | u8 | Layer/floor within the current room (see §2) | zelda3 `variables.h:141` | 272 273**Coordinate convention:** `link_x_coord`/`link_y_coord` ($7E0022/$7E0020) are used identically for overworld and dungeon movement — the same absolute pixel-coordinate fields, just interpreted relative to whichever map (overworld screen vs. dungeon room) is currently active per `player_is_indoors`. There is no separate "dungeon-local coordinate" field; the room/screen id ($7E00A0 or $7E008A) plus the pixel position together locate Link. This is confirmed structurally by both fields being used unconditionally throughout `src/player.c` and `src/overworld.c`/`src/dungeon.c` without an indoor/outdoor coordinate switch. 274 275--- 276 277## 5. Sprites, enemies, and ancillae (projectiles) 278 279Researched via a dedicated pass over zelda3 `src/sprite.c`, `src/sprite_main.c`, `src/ancilla.c`, `src/tile_detect.c`, `src/player.c`, cross-checked against `jpdasm` (JP1.0, incl. its `resources/memory_usage_sprite.txt` sprite-ID list) and `JaredBrian/AsarUSALTTPDisassembly` (`Other/symbols_wram.asm`). 280 281### 5.1 The 16-slot sprite tables 282 283All fields are 16-slot arrays, slot `k` at `base+k` (byte stride 1) — **confirmed by the main per-frame loop**: `Sprite_Main()` does `for (int i = 15; i >= 0; i--) { cur_object_index = i; Sprite_ExecuteSingle(i); }` (zelda3 `src/sprite.c:1141-1144`), and every field below is indexed `array[k]` throughout `sprite.c`/`sprite_main.c`. (The one exception, `sprite_N`, is also readable as a 16-bit array `sprite_N_word` over the same base — stride 2 when read as words, used on the overworld.) 284 285| Address | Name | Citation | Meaning | 286|---|---|---|---| 287| $7E0B58 | `sprite_stunned` | variables.h:656 | Auto-decrementing stun timer | 288| $7E0B6B | `sprite_flags` | variables.h:660 | Bitflags incl. tile-hitbox / deflect-arrows / boss-death bits (secondary AsarUSA `symbols_wram.asm:3190`) | 289| $7E0B89 | `sprite_obj_prio` | variables.h:666 | OAM object priority; set in `Sprite_TimersAndOam`, `src/sprite.c:1190,1204` | 290| $7E0BA0 | `sprite_ignore_projectile` | variables.h:672 | "Bulletproof" — nonzero means ancillae/projectiles don't interact with this sprite | 291| $7E0BB0 | `sprite_unk2` | variables.h:673 | Stores the ancilla ID that last hit this sprite (secondary AsarUSA:3298) | 292| $7E0BC0 | `sprite_N` (u8) / `sprite_N_word` (u16 alias, variables.h:1406) | variables.h:674 | Slot-in-room index; on the overworld it's a 16-bit pointer into the overworld death-list. Used by `Sprite_ManuallySetDeathFlagUW`, `src/sprite.c:3632-3636`, to permanently mark a sprite dead for the room (see 5.3) | 293| $7E0BE0 | `sprite_flags5` | variables.h:675 | Bitflags incl. tile-interaction/shield-block/damage-SFX/prize-pack (secondary AsarUSA:3376) | 294| $7E0C9A | `sprite_room` | variables.h:693 | Overworld screen/room the sprite belongs to (kills sprite on screen transition) | 295| $7E0CAA | `sprite_defl_bits` | variables.h:694 | Deflection bits; bit 0x80 = death-flag-ignore/pause-override (`src/sprite.c:1494-1498`), bit 0x01 = indoor-death-flag skip (`src/sprite.c:3633`) | 296| $7E0CBA | `sprite_die_action` | variables.h:695 | Forced drop on death (secondary AsarUSA:3656: 0=nothing,1=key,2=big key,3=green rupee) | 297| $7E0CD2 | `sprite_bump_damage` | variables.h:697 | Contact damage dealt to Link on touch | 298| $7E0CE2 | `sprite_give_damage` | variables.h:698 | Damage being dealt TO this sprite this frame; subtracted from `sprite_health`, `src/sprite.c:2409-2413` | 299| $7E0D00 | `sprite_y_lo` | variables.h:711 | Y position, low byte | 300| $7E0D10 | `sprite_x_lo` | variables.h:712 | X position, low byte | 301| $7E0D20 | `sprite_y_hi` | variables.h:713 | Y position, high byte | 302| $7E0D30 | `sprite_x_hi` | variables.h:714 | X position, high byte; combined via `Sprite_Get16BitCoords`, `src/sprite.c:1207-1210` | 303| $7E0D40 | `sprite_y_vel` | variables.h:715 | Y velocity | 304| $7E0D50 | `sprite_x_vel` | variables.h:716 | X velocity | 305| $7E0D60 | `sprite_y_subpixel` | variables.h:717 | Y sub-pixel accumulator | 306| $7E0D70 | `sprite_x_subpixel` | variables.h:718 | X sub-pixel accumulator | 307| $7E0D80 | `sprite_ai_state` | variables.h:719 | Per-sprite-type AI sub-state (bespoke meaning per handler), e.g. `src/sprite_main.c:8275` `if (sprite_ai_state[k]==3) // await death` | 308| $7E0D90..0DE0 | `sprite_A`,`sprite_B`,`sprite_C`,`sprite_D` | variables.h:720-725 | Generic per-type scratch bytes (bespoke meaning per handler; `sprite_C` is often reused as an HP-like counter by specific handlers) | 309| $7E0DC0 | `sprite_graphics` | variables.h:723 | Graphics/animation-frame control | 310| $7E0DD0 | `sprite_state` | variables.h:724 | **Top-level lifecycle dispatch index** — see 5.2/5.3 | 311| $7E0DF0/0E00/0E10 | `sprite_delay_main`/`_aux1`/`_aux2` | variables.h:726-728 | Auto-decrementing timers, `src/sprite.c:1171-1176` | 312| $7E0E20 | `sprite_type` | variables.h:729 | **Sprite type ID (0x00-0xF2)** — dispatch index into `kSpriteActiveRoutines[243]`, see 5.4 | 313| $7E0E30 | `sprite_subtype` | variables.h:730 | Per-type auxiliary subtype byte | 314| $7E0E40 | `sprite_flags2` | variables.h:731 | Bitflags incl. OAM-slot count (`&0x1f)+1)*4`, `src/sprite.c:1159`); secondary bits: harmless/master-sword-related/wall-related/OAM-count | 315| $7E0E50 | `sprite_health` | variables.h:732 | **Generic hit-point counter** — see 5.3 | 316| $7E0E60 | `sprite_flags3` | variables.h:733 | Bitflags: death-anim/impervious/shadow-size/has-shadow/OAM-palette/OAM-nametable | 317| $7E0E70 | `sprite_wallcoll` | variables.h:734 | Wall-collision-related flags | 318| $7E0EB0 | `sprite_head_dir` | variables.h:738 | Facing/movement direction | 319| $7E0EC0 | `sprite_anim_clock` | variables.h:739 | Animation clock | 320| $7E0EF0 | `sprite_hit_timer` | variables.h:742 | Hit/knockback-recoil timer; high bit = "recoiling" flag, `src/sprite.c:1180-1204` | 321| $7E0F00 | `sprite_pause` | variables.h:743 | Per-sprite pause flag, `src/sprite.c:1493-1498` | 322| $7E0F20 | `sprite_floor` | variables.h:745 | Floor/layer (0/1, BG1 vs BG2) | 323| $7E0F30/0F40 | `sprite_y_recoil`/`sprite_x_recoil` | variables.h:746-747 | Knockback/recoil offsets | 324| $7E0F50 | `sprite_oam_flags` | variables.h:748 | OAM attribute flags actually copied to hardware OAM | 325| $7E0F60 | `sprite_flags4` | variables.h:749 | Bitflags: ignore-collision / stasis (doesn't count toward room-clear) / activeness / hitbox ID | 326| $7E0F70/0F80/0F90 | `sprite_z`/`sprite_z_vel`/`sprite_z_subpos` | variables.h:750-752 | Height/jump coordinate, velocity, sub-position | 327 328Two single-byte (non-array) fields worth noting: `sprite_graphics_index` ($7E0AA3, variables.h:601) is a global sprite-graphics-load index unrelated to the per-slot block, and `sprite_limit_instance` ($7E0B6A, variables.h:659) is a global counter some sprite types (Beamos, Blind's phase) use to index among their own instances. 329 330A cached/"alt" mirror of the eight most important sprite fields exists at `$7E1D00` (`alt_sprite_state`, variables.h:836, one 16-slot array per field through `$7E1DF0`), used by `ExecuteCachedSprites()` (`src/sprite.c:4130-4193`) to save and restore sprite state outside the main per-frame loop — confirmed primary (`src/sprite.c:3574,4167`) and secondary-labeled "CACHE_0DD0"/"CACHE_0E20" in AsarUSA `Other/symbols_wram.asm:5093-5094`. A handful of `sprite_unk3/unk4/unk5`-style fields (variables.h:1216-1221) remain undocumented in **all four** cloned repos and are marked unverified rather than guessed. 331 332### 5.2 Alive/active detection — verified from the dispatcher itself 333 334```c 335// src/sprite.c:1212-1217 336void Sprite_ExecuteSingle(int k) { 337 uint8 st = sprite_state[k]; 338 if (st != 0) 339 Sprite_TimersAndOam(k); 340 kSprite_ExecuteSingle[st](k); 341} 342``` 343**`sprite_state[k] != 0` = "slot occupied."** `sprite_state` dispatches into `kSprite_ExecuteSingle[12]` (`src/sprite.c:462-475`): 0=inactive, 1=Fall1, 2=Poof, 3=Drown, 4=Explode, 5=Fall2, 6=Die, 7=Burn, 8=Initialize, **9=Active (normal AI)**, 10=Carried, 11=Stunned. So "alive and running its normal AI" specifically means `sprite_state[k]==9`, which then dispatches by `sprite_type[k]` into the per-type handler table (5.4). 344 345### 5.3 Three-tier alive/dead/never-spawned — verified, matches the harness's needs exactly 346 347Beyond the per-frame `sprite_state`, there is a **persistent per-room permanent-kill bitmask**, separate from any live slot: 348```c 349// src/sprite.c:3632-3636 350void Sprite_ManuallySetDeathFlagUW(int k) { 351 if (!player_is_indoors || sprite_defl_bits[k] & 1 || sign8(sprite_N[k])) 352 return; 353 sprite_where_in_room[dungeon_room_index2] |= 1 << sprite_N[k]; 354} 355``` 356`sprite_where_in_room` (`(uint16*)(g_ram+0x1DF80)` = **$7FDF80** — WRAM offset `0x1DF80`, which is the form Mesen's `snesWorkRam` memory type takes; variables.h:1201; overworld alias `sprite_where_in_overworld`, variables.h:1407) is a bitmask **per dungeon room**, one bit per spawn-list slot; `Dungeon_LoadSingleSprite` consults it on room load and never allocates a live slot for an already-permanently-killed spawn. So there are three real states: **not-yet-spawned-or-permanently-dead-this-visit** (bit set in `sprite_where_in_room[room]`, no live slot at all) / **occupies a live slot, `sprite_state`∈{1-8,10,11}** (dying/transitional/carried/stunned) / **occupies a live slot, `sprite_state==9`** (normal alive AI). 357 358**Health** — a real, generic, shared field exists: `sprite_health` ($7E0E50). Initialized per sprite type from `kSpriteInit_Health[243]` (`src/sprite.c:138`) inside `SpritePrep_LoadProperties` (`src/sprite.c:3967-3971`: `sprite_health[k] = kSpriteInit_Health[sprite_type[k]]`), and decremented by the common idiom `sprite_health[k] -= sprite_give_damage[k]` used by many (not all) handlers (e.g. `src/sprite.c:2409-2413`, `:2312`, `:4010`; kill-precheck `if (sprite_health[k] <= sprite_give_damage[k])`, `src/sprite.c:779`). Individual handlers remain free to repurpose or ignore it for scripted/multi-phase bosses (e.g. Giant Moldorm tests `sprite_health[k] < 3` for a phase change, `src/sprite_main.c:8262-8289`) — the mechanism is generic, per-type usage is bespoke on top of it. Whether the various soldier-guard handlers use only this generic path was not exhaustively confirmed for every soldier function; flagged partially-verified. 359 360### 5.4 Sprite type ID → name (verified, primary, exhaustively indexed) 361 362Dispatch table `kSpriteActiveRoutines[243]` (`src/sprite_main.c:465-710`), indexed directly by `sprite_type[k]` (`SpriteActive_Main`, `src/sprite_main.c:8257-8260`). The array position **is** the type ID — confirmed by counting entries and cross-matching several IDs embedded in function names (e.g. position 0x41=65 → `Sprite_41_BlueGuard`; position 0xD8=216 → `Sprite_D8_Heart`), and independently cross-checked against `jpdasm/resources/memory_usage_sprite.txt`, which lists the same IDs/names for the JP1.0 build (confirming the ID table is region-invariant). 363 364| ID | Name | Notes | 365|---|---|---| 366| $00 | Raven | | 367| $01 | Vulture | | 368| $02 | StalfosHead | | 369| $08, $0A | Octorok (2 color variants, shared handler) | | 370| $09 | GiantMoldorm | | 371| $0B | Cucco | | 372| $0C | OctorokStone | | 373| $0D | Buzzblob | | 374| $11 | Hinox | | 375| $12 | Moblin | | 376| $41-$43 | BlueGuard (×3) | soldier variant family | 377| $44 | BluesainBolt | soldier variant ("UsainBolt" in jpdasm — cosmetic naming difference only) | 378| $45 | HogSpearMan | soldier variant | 379| $46 | BlueArcher | soldier variant | 380| $47 | GreenBushGuard | soldier variant | 381| $48 | RedJavelinGuard | soldier variant | 382| $49 | RedBushGuard | soldier variant | 383| $4A | BombGuard | soldier variant | 384| $4B | GreenKnifeGuard | soldier variant | 385| $4C | Geldman | | 386| $4D | Toppo | | 387| $6D | **Rat** | | 388| $6E | **Rope** | | 389| $6F | **Keese** (bat) | | 390| $73 | **Uncle and Priest** | | 391| $76 | **Zelda** | | 392| $78 | Mrs. Sahasrahla | | 393| $7A | Agahnim | | 394| $B4 | Purple Chest | the *only* sprite-table chest (roaming quest item) — see note below on ordinary chests | 395| $C7 | Pokey | | 396| $D8 | **Heart drop** | | 397| $D9-$DB | **Rupee drop** (Green/Blue/Red, per jpdasm cross-check) | | 398| $E4-$E5 | **Key drop** (Small Key / Big Key, per jpdasm cross-check) | | 399| $EA | Heart Container | | 400| $EB | Heart Piece | | 401 402**Pots, bushes, and ordinary dungeon chests are confirmed map/tile objects, NOT sprites** — no pot/bush entry exists anywhere in the 243-entry table. Primary evidence: `src/tile_detect.c:391-404` handles tile IDs `0x50-0x56` under `// TileBehavior_Liftable`; `src/player.c:35`'s `kLink_Lift_tab[9] = {0x54,0x52,0x50,0xFF,0x51,0x53,0x55,0x56,0x57}` keys Link's lift action directly off these tile IDs. Ordinary dungeon chests are likewise tile-driven: `src/tile_detect.c:410-422`, tile IDs `0x58-0x5D`/`0x63`, gated by array `dung_chest_locations` (`(uint16*)(g_ram+0x6E0)` = $7E06E0, variables.h:581) — **not** the sprite table. (`Sprite_B4_PurpleChest` above is the one deliberate exception: a roaming quest-item chest, not the per-room treasure mechanic.) When a lifted pot/bush/rock hides a secret, the reveal spawns a genuine new sprite via `Sprite_SpawnSecret` (`src/sprite.c:1065`), and a destroyed bush's poof is ancilla type 63 (`Ancilla3F_BushPoof`, §5.6). 403 404### 5.5 Ancilla (projectile) table — verified, primary, exhaustive 405 406`grep -n "ancilla" src/variables.h`: single **10-slot** table (loop bound confirmed `for (int i = 9; i >= 0; i--)`, `src/ancilla.c:662`), base `$7E0BF0`, stride 0x0A (10) per field: 407 408| Address | Name | Citation | Meaning | 409|---|---|---|---| 410| $7E0BFA | `ancilla_y_lo` | variables.h:677 | Y position low byte | 411| $7E0C04 | `ancilla_x_lo` | variables.h:678 | X position low byte | 412| $7E0C0E | `ancilla_y_hi` | variables.h:679 | Y position high byte | 413| $7E0C18 | `ancilla_x_hi` | variables.h:680 | X position high byte | 414| $7E0C22 | `ancilla_y_vel` | variables.h:681 | Y velocity | 415| $7E0C2C | `ancilla_x_vel` | variables.h:682 | X velocity | 416| $7E0C4A | `ancilla_type` | variables.h:685 | **Type ID, 1-based** (0=empty slot) — dispatch `src/ancilla.c:661-677` | 417| $7E0C54 | `ancilla_step` | variables.h:686 | Per-type step/sub-state scratch | 418| $7E0C5E | `ancilla_item_to_link` | variables.h:687 | Tracks hookshot extension / item-receipt ID (secondary AsarUSA:3534) | 419| $7E0C68 | `ancilla_timer` | variables.h:688 | General auto-decrementing timer, `src/ancilla.c:674-675` | 420| $7E0C72 | `ancilla_dir` | variables.h:689 | Direction | 421| $7E0C7C | `ancilla_floor` | variables.h:690 | Floor/layer (0/1), `src/ancilla.c:764-766` | 422| $7E0C90 | `ancilla_numspr` | variables.h:692 | Number of OAM sprites this ancilla uses | 423 424### 5.6 Ancilla type IDs — complete, from `kAncilla_Funcs[67]` (`src/ancilla.c:252-320`, 1-indexed via `ancilla_type-1`) 425 426| ID | Meaning | ID | Meaning | 427|---|---|---|---| 428| 1 | Somaria bullet | 2 | Fire Rod shot | 429| 4 | Sword-beam impact | **5** | **Boomerang** | 430| 6 | Generic wall-hit | **7** | **Bomb** | 431| 8 | Door debris | **9** | **Arrow** | 432| 10 | Arrow stuck in wall | 11 | Ice Rod shot | 433| **12** | **Sword beam** (full-health charge) | 13 | Full-charge spin spark | 434| 17 | Ice Rod wall-hit | 19 | Ice Rod sparkle | 435| 21 | Jump/water splash | 22 | Hit-stars stun effect | 436| 24 | Ether medallion | 25 | Bombos medallion | 437| 26 | Magic Powder dust | 28 | Quake medallion | 438| **31** | **Hookshot** | 34 | Item-get animation | 439| 40 | Wishing-pond item toss | **49** | **Cane of Byrna** shield spark | 440| 51 | Blast-wall explosion | 53 | Master Sword receipt | 441| 54 | Flute effect | 58 | Big Bomb explosion | 442| **63** | **Bush destruction poof** | 67 | Ganon's Tower cutscene effect | 443 444(Full 1-67 table available in the underlying research; this is the subset most relevant to early-game automation. No web search was needed for either the sprite or ancilla ID tables — `src/sprite_main.c` and `src/ancilla.c` gave complete, cleanly-named primary tables directly.) 445 446--- 447 448## 6. Dungeon room state and tile-attribute/collision maps 449 450Researched via a dedicated pass over zelda3 `src/dungeon.c`, `src/tile_detect.c`, `src/overworld.c`, cross-checked against `jpdasm`/`usdasm` and `JaredBrian/AsarUSALTTPDisassembly/Logs/Zelda_3_SRM.log`. 451 452### 6.1 SRAM room-flags mirror ($7EF000) 453 454`save_dung_info ((uint16*)(g_ram+0xF000))` = **$7EF000**, one 16-bit word per dungeon room (room index 0-0x13F used of a 0x500-byte/0x280-room table — size confirmed by `memcpy(..., save_dung_info, 0x500)`, `src/messaging.c:259`). Zelda3 `src/variables.h:1060`. 455 456Bit layout (derived from the save/load code, `src/dungeon.c:8059-8064` and `:3724-3727`), confirmed independently by JaredBrian's `Logs/Zelda_3_SRM.log:92-125` which diagrams the identical structure: 457 458``` 459bit: 15 14 13 12 | 11 10 | 9 8 7 6 5 4 | 3 2 1 0 460 d d d d | b k | u t s e h c | q q q q 461``` 462(Corrected/reconciled diagram: an earlier draft of this table mis-split bits 4-9 into "cccc"+"ck"+"cr", which undercounted the chest bits and used undefined labels; the letters above follow jpdasm's own bit-comment, `jpdasm/symbols_sram.asm:56-69`, and are independently reproduced by the zelda3 evidence below — the two fully agree once read carefully.) 463- **bits 0-3 (`qqqq`)** = quadrant-visited flags (`dung_quadrants_visited`, live at $7E0408) 464- **bits 4-9 (`cehstu`)** = `kChestOpenMasks[] = {0x100,0x200,0x400,0x800,0x1000,0x2000}` (`src/dungeon.c:112`, pre-shift bits 8-13, landing at post-shift bits 4-9) — **6 chest-open flags**, set in `OpenChestForItem` (`src/dungeon.c:5717-5752`). Per jpdasm's naming these are chest slots 0-5 (`c`,`h`,`e`,`s`,`t`,`u`); zelda3's own evidence shows two of the six slots doing double duty in specific rooms — bit 0x800 (chest slot "t"/bit 7... **correction, see below**) is also reused as **water-gate/torches-lit puzzle solved** in rooms tagged for it (`src/dungeon.c:4772,4809,4828,4840`), and bit 0x1000 ("u"/bit 9-equivalent pre-shift) doubles as "special rupee tile obtained" (`src/dungeon.c:5698`) — jpdasm's own comment says the same thing in different words ("chest 4 / rupee floor / swamp drains / bombable floor / mire wall" for `t`, "chest 5 / 2nd key / heart piece" for `u`). **This is genuine room-tag-dependent polymorphism, not a labeling error**: the same bit means "chest" in a room that has a chest there and something else in a room that doesn't. 465- **bit 10 (`k`)** = key / heart piece / crystal taken (crystal bit unused for the actual crystal per jpdasm, but still prevents it re-dropping) — pre-shift bit 0x4000 466- **bit 11 (`b`)** = boss defeated / heart container taken — **one single consistent meaning**, pre-shift bit 0x8000, set in `PrepareDungeonExitFromBossFight` (`src/dungeon.c:2497`), read for crystal/pendant rewards (`src/dungeon.c:4584,4596`) 467- **bits 12-15 (`dddd`)** = `dung_door_opened` — up to 4 unlockable/breakable doors in the room (live at $7E0400) 468 469Live working-register addresses these mirror to/from (loaded from SRAM on room entry, written back on exit/quadrant-cross): `dung_door_opened`=$7E0400 (variables.h:400), `dung_savegame_state_bits`=$7E0402 (variables.h:401), `dung_quadrants_visited`=$7E0408 (variables.h:403), plus a derived (non-persisted) `dung_door_opened_incl_adjacent`=$7E068C (variables.h:569) that merges in currently-open doors of adjacent rooms. Overworld one-time-event-flags mirror: `save_ow_event_info` ((uint8*)(g_ram+0xF280)) = **$7EF280**, one byte per overworld screen (variables.h:1061). 470 471### 6.2 Current room/dungeon ID — confirmed exactly, with one nuance 472 473`dungeon_room_index`=$7E00A0, `overworld_screen_index`=$7E008A, `cur_palace_index_x2`=$7E040C — all exactly as expected (§4). **Nuance:** `cur_palace_index_x2`'s raw stored value is the dungeon/palace index **already multiplied by 2** (used directly as a word-table index elsewhere, e.g. `src/dungeon.c:5717`); the real dungeon number is `WRAM[$7E040C] >> 1`. Independently confirmed by jpdasm's own comment: `symbols_wram.asm:2007` — `DUNGEON = $7E040C ; "Dungeon IDs, multiples of 2."` `dungeon_room_index_prev`=$7E00A2 (variables.h:113) holds the room being transitioned *from*, valid during a transition rather than a general history slot. 474 475**Cache-vs-live distinction:** `save_dung_info[room]` (§6.1) is the persistent record; `dung_door_opened`/`dung_savegame_state_bits`/`dung_quadrants_visited` are the live working copy for whichever room Link currently occupies, reloaded from the SRAM mirror on room entry and flushed back on exit — reading the SRAM mirror for the *current* room can be one frame stale; for any other room it's authoritative. 476 477### 6.3 Dungeon tile attribute / collision map — the core walkability-grid mechanism 478 479**Base addresses**, both independently confirmed (jpdasm `symbols_wram.asm:6002-6003`, `usdasm` bank files): 480- `dung_bg2_attr_table` = **$7F2000** (variables.h:1161) 481- `dung_bg1_attr_table` = **$7F3000** (variables.h:1162) 482 483(Naming discrepancy only, not a value discrepancy: the disassemblies call $7F2000/$7F3000 "COLMAPA/COLMAPB" or "Layer 1/Layer 2" with the opposite BG-layer label from zelda3's `bg2`/`bg1` naming — the addresses and one-byte-per-8×8-tile mechanism agree exactly across all sources.) 484 485**Dimensions:** a flat **64×64 grid, one byte per 8×8-pixel cell** = 4096 bytes per table (0x1000), the two tables back-to-back ($7F2000-$7F2FFF then $7F3000-$7F3FFF) — i.e. each dungeon room is 512×512 pixels. Confirmed via `tilemap_location_calc_mask = 0x1f8` for dungeons (`src/dungeon.c:8340,8384`). 486 487**Encoding:** direct byte value read straight into the tile-behavior switch — `tile = dung_bg2_attr_table[offs]; TileDetect_ExecuteInner(tile, ...)` (`src/tile_detect.c:246,253`) — **not** an extra indirection at read time. (It IS populated from a separate ROM-driven lookup, `attributes_for_tile` = $7EFE00, variables.h:1138, during room construction — confirmed via `attributes_for_tile[dung_bg2[i] & 0x3ff]`, `src/dungeon.c:3842` — but by the time Link's tile-detection code runs, the byte in the $7F2000/$7F3000 grid is already the final behavior value, ready to switch on directly.) 488 489**Pixel → index formula (exact, from `TileDetection_Execute`, `src/tile_detect.c:240-254`):** 490``` 491index = (pixelY >> 3) * 64 + (pixelX >> 3) // row-major, 64-wide 492addr = $7F2000 + index + (link_is_on_lower_level ? 0x1000 : 0) 493``` 494 495**Correction, 2026-09-20 — the coordinate must be folded into the room FIRST, and this formula alone will not tell you so.** `link_x_coord`/`link_y_coord` are absolute across the whole dungeon map, not room-local: Link standing in his own house reads `x = 2413, y = 8528`. The grid is 512×512 pixels, one room. The engine masks before it indexes — `((link_x + offset) & tilemap_location_calc_mask) >> 3`, with the mask set to `0x1f8` for indoors (`src/tile_detect.c:37-40`; mask at `src/dungeon.c:8340,8384`) — so the whole formula is: 496``` 497column = ((x & 0x1F8) >> 3) // 0..63 498row = ((y & 0x1F8) >> 3) // 0..63 499index = row * 64 + column 500``` 501Measured cost of not doing this: every direction reported "wall" and the nearest doorway was "8289 px up and left". The harness was asking a coherent question about a room that did not exist, and nothing in the answers showed it. 502 503**Where the engine samples, when Link tries to move** (`src/tile_detect.c:7-10, 30-61`): going up, the row at `y + 8`; going down, the row at `y + 24`; going left or right, the column at `x + 0` or `x + 15`. Three points each time — across his width (`x + 0, +8, +15`) for vertical movement, down his body (`y + 8, +16, +23`) for horizontal — which it then resolves with sliding rules of its own. A harness that reports the worst of the three makes Link's own shoulder into a wall: standing beside a wall on his right, up, down and right all come back solid. The middle point is the honest one-value summary. 504`link_is_on_lower_level` ($7E00EE, §2) is exactly the flag selecting the $7F2000 vs. $7F3000 half. 505 506**Attribute value meanings — the switch is real and primary-verified** (`TileDetect_ExecuteInner`, `src/tile_detect.c:261-525`, a complete `switch(tile)` over 0x00-0xFF), cross-checked against jpdasm's fully-annotated `values.asm:663-870` tile-type comment table: 507 508| Value(s) | Meaning | 509|---|---| 510| `0x00` | Standard floor (walkable) | 511| `0x01-0x04, 0x26, 0x43` | Solid / wall collision | 512| `0x08` | Deep water | 513| `0x09` | Shallow water | 514| `0x0D` | Spike floor | 515| `0x0E`/`0x0F` | Ice (GT / Ice Palace) | 516| `0x10-0x13, 0x18-0x1B` | Diagonal slopes (4 orientations ×2 variants) | 517| `0x1D-0x1F, 0x3D-0x3F` | Auto-stairs / layer-swap (N/S) | 518| `0x20, 0xB0-0xBD` | Pit / Somaria-platform-pit variants | 519| `0x22, 0x30-0x39` | Manual and straight inter-room stairs | 520| `0x28-0x2F` | Ledges (N/S/E/W + diagonal) | 521| `0x44` | Spike | 522| `0x46` | Desert Palace tablet trigger | 523| **`0x50-0x56`** | **Liftable objects** (bush/rock/sign variants) | 524| **`0x58-0x5D`** | **Chest 0-5** (value directly encodes which chest slot, matching `kChestOpenMasks` index, §6.1) | 525| `0x5E-0x5F` | Spiral stairs | 526| `0x60` | Rupee tile | 527| `0x63` | Minigame chest | 528| `0x67-0x6B` | Crystal peg / conveyors | 529| `0x80-0xAF` | Door/shutter-door/layer-toggle-door family (many sub-types; several even jpdasm marks "(?)" — treat exact 0x88/0x8A-0x8D/most of 0x90-0xAF as uncertain, only the general "door" category is solid) | 530| `0xC0-0xCF` | Lightable torches | 531| `0xD0-0xFF` | Mostly unused; `0xF0-0xFF` zelda3 labels `TileBehavior_FlaggableDoor` | 532 533### 6.4 Overworld tile attribute / collision — structurally DIFFERENT from dungeons, verified 534 535There is **no flat pre-resolved attribute grid for the overworld** the way $7F2000/$7F3000 exist for dungeons. Instead: `overworld_tileattr` = **$7E2000** (u16 array, variables.h:1416; aliases the dungeon's `dung_bg2` address — mutually exclusive by mode) holds **map16 tile IDs**, not attribute bytes, in a 32×32 grid (normal screen, 512×512px) or 64×64 grid (a "big" overworld area, 1024×1024px), selected via `overworld_area_is_big`. Turning a map16 ID into a walkability value requires a **two-stage ROM-asset lookup**, not WRAM: `GetMap16toMap8Table()`/`GetMap8toTileAttr()` (`src/overworld.c:246-251`, decompressed ROM data via `g_asset_ptrs`), final value `GetMap8toTileAttr()[t & 0x1ff]` (`src/tile_detect.c:24`). The resulting byte feeds into the **same** `TileDetect_ExecuteInner` switch as §6.3, just with `is_indoors=false` selecting a few overworld-only cases (tall grass `0x04`, gravestones `0x42`). **Practical implication for the harness:** an overworld walkability grid cannot be read as a single WRAM byte array the way a dungeon room can — either replicate the two ROM lookup tables once (they appear to be static, not verified whether they vary per overworld area) or call the equivalent lookup logic. 536 537**Done, 2026-09-20 — the lookup is ported, and the tables are plain ROM.** `packages/alttp/src/outside.rs` implements `Overworld_GetTileAttributeAtLocation` (zelda3 `src/overworld.c:14-28`) against whichever ROM the app booted, so nothing is hard-coded: 538 539| Table | SNES address | Size | zelda3's own extractor | 540|---|---|---|---| 541| `kMap16ToMap8` | `$8F8000` | 3752 × 4 words | `assets/compile_resources.py:155` | 542| `kMap8DataToTileAttr` | `$8E9459` | 512 bytes | `assets/compile_resources.py:436` | 543 544Both are **uncompressed** — zelda3's asset build reads them with a bare `ROM.get_words`/`get_bytes`, with no decompression step, which is what makes replicating the lookup a dozen lines rather than a project. LoROM stepping is `offset = ((addr >> 16) & 0x7F) * 0x8000 + (addr & 0x7FFF)`, and a read that walks off the top of a bank continues at the NEXT bank's `$8000`, never its `$0000` (`assets/util.py:89-116`); getting that second half wrong reads eight kilobytes of the wrong bank and yields a map that is almost right. 545 546The coordinate frame comes from WRAM: `overworld_offset_base_y` = `$7E0708`, `overworld_offset_mask_y` = `$7E070A`, `overworld_offset_base_x` = `$7E070C`, `overworld_offset_mask_x` = `$7E070E` (zelda3 `src/variables.h:584-587`), with the map16 ids at `$7E2000` (`variables.h:1416`). 547 548**A handful of attribute values mean something else outdoors**, and reading them the indoor way makes the overworld unwalkable: `0x04` is tall grass rather than a wall, `0x42` is a gravestone, and the long `TileBehavior_NothingOW` case list (`src/tile_detect.c:261`: `0x05-0x07`, `0x14-0x17`, `0x21`, `0x23-0x25`, `0x38-0x3C`, `0x41`, `0x45`, `0x47`, `0x49`, `0x5E-0x5F`, `0x61-0x62`, `0x64-0x66`, `0xA6-0xA7`, `0xBE-0xBF`, `0xD0-0xEF`) is plain ground out there. Verified against the real USA ROM 2026-09-20: standing outside Link's house the harness reads a bush to the left, floor to the right and the stream as deep water to the east. 549 550### 6.5 Door/stairs tables — ROM-resident per room, not a persistent WRAM table (verified, not assumed) 551 552`GetRoomDoorInfo(room)` reads from ROM asset tables `kDungeonRoom`/`kDungeonRoomDoorOffs` (`src/dungeon.c:2256-2258`) — confirmed doors are reloaded from ROM per room. A **transient per-current-room cache** does exist: `dung_door_tilemap_address` = **$7E19A0** (u16 array, variables.h:799), populated from the room's ROM door list until a `0xFFFF` terminator on room load (`src/dungeon.c:3728-3732`) — useful if the harness only needs the doors of the room Link is in right now (exact per-entry bit-packing of position/type not decoded in this pass). Regular-door destinations are structural (the adjacent room in the dungeon's grid layout, via `Dungeon_CheckAdjacentRoomsForOpenDoors`, `src/dungeon.c:3746-3803`) rather than an explicit stored target. **Explicit non-adjacent warp targets** (staircases, holes, block-push warps) ARE WRAM-cached: `dung_hdr_travel_destinations` = **$7EC000** (5 bytes, from room header, `src/dungeon.c:3715-3719`; address independently re-verified: `variables.h:930`, `(uint8*)(g_ram+0xC000)`), consumed at transition points e.g. `dungeon_room_index = dung_hdr_travel_destinations[3]` (`src/dungeon.c:2067`) — the index-to-staircase-slot correspondence was not fully traced in this pass, flagged partially-verified. Related staircase-position caches: `dung_hdr_staircase_plane`=$7E063D (variables.h — 4 entries, 2-bit plane each), `dung_stairs_table_1`=$7E06B8, `dung_stairs_table_2`=$7E06EC (variables.h:582,1421) — addresses and stated purpose confirmed, exact per-entry format not fully traced. 553 554--- 555 556## 7. Dialog / text box / menu detection 557 558| Address | Name | Size | Meaning | Citation | 559|---|---|---|---|---| 560| `$7E1CD4` | `text_render_state` | u8 | Index into the 5-stage text-render sub-state-machine (0=Border,1=BorderIncremental,2=CharacterTilemap,3=MessageCharacters,4=Finish) | zelda3 `variables.h:816`; table `src/messaging.c:115-121` | 561| `$7E1CD8` | `messaging_module` | u8 | Sub-mode within the messaging system | zelda3 `variables.h:820` | 562| `$7E1CE0` | `text_wait_countdown` | u16 | Generic text timing countdown | zelda3 `variables.h:825` | 563| `$7E1CE8` | `choice_in_multiselect_box` | u8 | Which option is highlighted in a 2/3-choice text prompt | zelda3 `variables.h:828` | 564| `$7E1CE9` | `text_wait_countdown2` | u8 | Post-keypress debounce countdown (28 frames) used by both the "wait for key" and "end message" text commands | zelda3 `variables.h:829`; usage `src/messaging.c:2469-2489` | 565| `$7E1CF0` | `dialogue_message_index` | u16 | **Which dialogue message is loaded/showing** | zelda3 `variables.h:833` | 566 567**Text box open/closed — exact rule, verified from source:** 568``` 569text_box_open := (main_module_index == 14) and (submodule_index == 2) 570``` 571`submodule_index == 2` for module 14 dispatches to `RenderText` (`kMessagingSubmodules[12]`, index 2 → `&RenderText`, `src/messaging.c:184-197`; dispatch `RunInterface()` at `src/messaging.c:308-310`). When the box closes, `RenderText_Draw_Finish` (`src/messaging.c:2499-2509`) sets `submodule_index = 0` and restores `main_module_index = saved_module_for_menu` — i.e. control returns to whatever gameplay module (7/9/11) was active before the text triggered, confirming `saved_module_for_menu` ($7E010C) is exactly "the module to check for post-dialog." 572 573**Awaiting a button press to advance/close — verified, but not a single flag:** the in-message byte-stream commands `kTextCmd_Waitkey` (23) and `kTextCmd_EndMessage` (24) are handled inline inside `RenderText_Draw_MessageCharacters` (`src/messaging.c:2469-2490`): execution stalls at that command, checking `(filtered_joypad_H | filtered_joypad_L) & 0xc0` (Waitkey, A/B mask) or any button at all (EndMessage), and only advances/sets `text_render_state = 4` once pressed. There is **no separate WRAM boolean** meaning "awaiting input" — the practical, reliable harness heuristic is: `text_box_open == true` **and** `text_render_state` has not changed for ≥1 frame while `dialogue_msg_read_pos` (message byte cursor, not separately tabulated above) is also unchanged — i.e. render is stalled. This is a legitimate engine property (the pause commands are literally spin-loops on joypad state), not a guess, but it is a *derived* condition rather than one memory cell — flagged accordingly. 574 575**Menu/pause-related submodules of module 14** (table `kMessagingSubmodules[12]`, `src/messaging.c:184-196`, dispatch `src/messaging.c:308-310` `kMessagingSubmodules[submodule_index]()`): 576 577| `submodule_index` | Function | Meaning | 578|---|---|---| 579| 0 | `Module_Messaging_0` | Unused stub (`assert(0)`) | 580| 1 | `Hud_Module_Run` | HUD update pass | 581| 2 | `RenderText` | **Text box actively rendering** (see rule above) | 582| 3 | `Module0E_03_DungeonMap` | Dungeon map screen (opened via the X/map button in a dungeon, gated `src/dungeon.c:6602-6608` on having a valid palace+room) | 583| 4 | `Module0E_04_RedPotion` | Drinking a red potion (HP refill animation) | 584| 5 | `Module0E_05_DesertPrayer` | Desert Palace opening-prayer cutscene | 585| 6 | `Module_Messaging_6` | Unused stub | 586| 7 | `Messaging_OverworldMap` | **World map screen** (opened via Select/map button on the overworld, `src/overworld.c:760-763`) | 587| 8 | `Module0E_08_GreenPotion` | Drinking a green potion (magic refill) | 588| 9 | `Module0E_09_BluePotion` | Drinking a blue potion (both refill) | 589| 10 | `Module0E_0A_FluteMenu` | Flute-warp menu | 590| 11 | `Module0E_0B_SaveMenu` | **Save/continue/quit prompt** (the closest thing to a "pause menu" — text notes it's specifically "the continue / save and quit menu", `src/messaging.c:356-357`) | 591 592**How Start/Select enter module 14** (both confirmed by the same source lines that establish the player-control rule in §1): pressing Start while `player_has_control` sets `submodule_index = 1`, `saved_module_for_menu = main_module_index`, `main_module_index = 14` (`src/overworld.c:751-758`, `src/dungeon.c:6595-6601` — identical pattern); pressing the map button similarly sets `submodule_index` to 3 (dungeon map) or 7 (world map). 593 594**Item-get / "fanfare" indicator:** not independently isolated as a single dedicated flag in this pass — `link_disable_sprite_damage` and `link_player_handler_state == kPlayerState_HoldUpItem (21)` co-occur during an item pickup, but neither is exclusive to that moment (both are shared with other cutscene-like states). **Flagged as not cleanly resolved from source in this session** — a harness wanting a crisp "item-get fanfare active" signal should treat `main_module_index==14` (dialog open, since most item-get text boxes route through the messaging system) combined with `link_player_handler_state==21` as the best available composite, not a single verified flag. 595 596--- 597 598## 8. Intro sequence (file select → rescuing Zelda) 599 600**Modules, all verified in §1's table** (zelda3 `src/misc.c:151-180`): Title/attract = module 0 (`Module00_Intro`) or module 20 (`Module14_Attract`, entered after idling on the title screen); File select = module 1; Copy file = module 2; Delete file = module 3; Name entry (new file) = module 4; Load file = module 5; first real gameplay module after loading = module 6 (`Module_PreDungeon`, a brief transition) then module 7 or 9/11 depending on where the save point is. 601 602**The opening "asleep in bed" state:** `kPlayerState_AsleepInBed` (value 22, §2) is a named, explicit state in the same enum as `Ground`/`Swimming`/etc — this is Link's `link_player_handler_state` ($7E005D) value during the rainy-night opening in his house before Zelda's telepathic message wakes him. This is a solid, source-named signal for "we're in the very-opening cutscene," found by direct enum inspection (`src/player.h:27`) rather than a targeted intro-specific search — **the mechanism is verified, but this pass did not trace the exact function that sets/clears it (e.g. `src/misc.c` or a dedicated opening-sequence file) to confirm timing relative to the rain/telepathy dialogue**, so treat "state==22 ⇒ specifically the opening bed cutscene" as **highly likely but not 100%-source-traced in this pass** (the value is real; its exclusivity to the intro specifically, vs. any other "asleep in bed" moment such as a later inn/house sleep, was not separately checked). 603 604**Room/entrance/area IDs for the opening segment (Link's house, secret passage, Uncle, Hyrule Castle, rescuing Zelda):** **UNVERIFIED in this pass.** Neither the zelda3 C source (no dedicated "opening sequence" file with named room/entrance constants was located — the intro's room transitions are driven by generic entrance-table data loaded from ROM, not hardcoded named constants in the reimplementation) nor the disassemblies' bank files were searched down to the specific numeric room/entrance IDs for this segment within this session's time budget. This is exactly the kind of fact this estate's sourcing rules require flagging rather than guessing: **do not trust any specific numeric value for "Link's house room id" or "secret passage entrance id" until a follow-up pass greps `usdasm`'s `rooms.asm`/`overworlds.asm` and/or JaredBrian's per-room files for the vanilla intro's actual constants, or the harness empirically logs `$7E00A0`/`$7E008A`/`$7E010E` while manually playing the opening on a real emulator.** The practical, robust alternative already fully verified above: an automation harness can detect "intro complete, first free control granted" purely from state transitions already nailed down here — `main_module_index` reaching 7/9/11 with `submodule_index==0` and the player-control gate flags all clear (§1) — without needing any hardcoded room-id table at all, which is arguably the more robust design for a harness than pinning specific IDs. 605 606**First required actions (per the well-known, non-controversial structure of the vanilla opening, not itself a WRAM fact needing citation):** wake up → leave the house → go around to the castle's secret passage (bush-covered entrance on the north wall) → navigate to find Uncle wounded → receive sword and shield from him → proceed into the castle → rescue Zelda from the dungeon cell → escape via the sewers/secret passage back out. This sequence description is common knowledge about the game's structure, not a memory-address claim, so it carries no file:line citation and is not treated as equivalent in rigor to the address tables above. 607 608--- 609 610## 9. ROM identification (verified: zelda3 source code + spannerisms/usdasm README + web search attributed to No-Intro — three independent agreements) 611 612| Property | Value | Source | 613|---|---|---| 614| Unheadered file size | 1,048,576 bytes (0x100000) | Standard SNES LoROM size for this cart; confirmed indirectly by zelda3's header-strip check `(len(self.ROM) & 0xfffff) == 0x200` — i.e. it treats any file whose size mod 0x100000 is exactly 0x200 as headered, else assumes the bare 0x100000 image (zelda3 `assets/util.py:63-65`) | 615| Headered file size | 1,049,088 bytes (0x100200) — unheadered size + 512-byte copier header | zelda3 `assets/util.py:63-65` (same check) | 616| Header strip method | If `len(ROM) % 0x100000 == 0x200`, drop the first `0x200` (512) bytes | zelda3 `assets/util.py:63-65`, verbatim: `if (len(self.ROM) & 0xfffff) == 0x200: self.ROM = self.ROM[0x200:]` | 617| USA v1.0 SHA-1 (unheadered) | `6D4F10A8B10E10DBE624CB23CF03B88BB8252973` | zelda3 `assets/util.py:15` (`ZELDA3_SHA1_US`, hard-gates asset extraction to exactly this ROM) — **independently matches** spannerisms/usdasm `README.md` (SHA1 line, unheadered) **and** a web search result attributed to the No-Intro SNES DAT (retrieved 2026-09-20; the aggregator page itself wasn't independently opened, so grade this leg of the triangulation as secondary, but it agrees byte-for-byte with the two primary/source-code citations) | 618| USA v1.0 MD5 (unheadered) | `608C22B8FF930C62DC2DE54BCD6EBA72` | spannerisms/usdasm `README.md` (Binaries section); matches the No-Intro-attributed web search result | 619| USA v1.0 CRC32 (unheadered) | `777AAC2F` | spannerisms/usdasm `README.md`; matches the No-Intro-attributed web search result | 620| USA v1.0 CRC32 (headered, per web search only) | `DD42510E` | Web search only, attributed to No-Intro — **not cross-checked against a cloned source in this session**, since the disassemblies work from unheadered ROMs only. Treat as secondary until independently confirmed. | 621| Internal ROM checksum (SNES header, complement pair) | `AF0D` / `50F2` | spannerisms/usdasm `README.md` — this is the SNES cartridge header's internal checksum, distinct from CRC32/MD5/SHA1 file hashes; useful as a fast in-ROM sanity check without hashing the whole file | 622 623**How to apply for the harness:** read the loaded file's size; if `size % 0x100000 == 0x200`, skip the first 512 bytes before treating offsets as raw ROM offsets (WRAM addresses in this document are unaffected either way — WRAM is emulator RAM, not ROM, and has no header). SHA-1 of the (header-stripped) file should equal `6D4F10A8B10E10DBE624CB23CF03B88BB8252973` to guarantee USA v1.0. 624 625--- 626 627## Open items — not resolved in this pass, do not treat as answered 628 6291. ~~**Exact direction-value mapping for `link_direction_facing` ($7E002F)**~~ — **RESOLVED 2026-09-20, from source, not empirically.** `0 = up, 2 = down, 4 = left, 6 = right`. Two independent places in zelda3 pin it: `kDashTab2[] = {8, 4, 2, 1}` is indexed by `link_direction_facing >> 1` and used as the joypad bits that would continue a dash (`src/player.c:1184,1212`), and those bits are Up/Down/Left/Right (`src/zelda_rtl.h:89-92`); and `src/misc.c:706-708` sets `link_direction = 8` (up) and `link_direction_facing = 0` in the same breath. (`kGrabWallDirs[] = {4, 8, 1, 2}` is indexed the same way and looks inverted; it is the direction that must be HELD to keep a wall grab, not the facing, and is not evidence against the above.) Implemented as `alttp::Direction::facing`. 6302. **Intro-segment numeric room/entrance/area IDs** (Link's house, secret passage, Uncle's room, Hyrule Castle interior, Zelda's cell) — flagged unverified in §8; the reimplementation drives the intro from generic ROM entrance-table data with no named constants, and this pass did not chase the numeric values down in `usdasm`'s `rooms.asm`/`overworlds.asm`. The player-control detection rule in §1 gives a room-id-free way to detect "intro complete" for a harness, which may make this item lower priority than it first appears. 6313. **Door/staircase entry bit-packing** — `dung_door_tilemap_address` ($7E19A0) and `dung_hdr_travel_destinations` ($7EC000) addresses and top-level purpose are confirmed (§6.5), but the exact per-entry field-packing (position/type within each door word; which travel-destination array index corresponds to which staircase/hole) was not decoded in this pass. 6324. **Overworld map16→attribute ROM tables** (`kMap16ToMap8`/`kMap8DataToTileAttr`, §6.4) — confirmed to be ROM-asset-resident rather than WRAM, but whether they vary per overworld area index (vs. one fixed table for the whole overworld) was not confirmed. 6335. **A handful of obscure dungeon door sub-type tile values** (`0x88`, `0x8A-0x8D`, most of `0x90-0xAF`, §6.3) — even jpdasm's own annotated tile-type table marks these uncertain ("(?)"); only the general "this is a door" category is solid for these specific values. 6346. **Some `sprite_unk3/unk4/unk5`-style scratch fields** (variables.h:1216-1221) and a few `alt_sprite_*` cache fields in the `$7E1FA6C-$7E1FADC` range (§5.1) — undocumented in all four cloned repos, not guessed at. 6357. **Whether every soldier-guard sprite handler uses only the generic `sprite_health` damage path** (§5.3) — no bespoke HP override was found in the handlers read, but not every soldier function body was read exhaustively. 6368. **Item-get/"fanfare" as a single clean flag** (§7) — no dedicated WRAM boolean was isolated; the best available composite (`main_module_index==14` + `link_player_handler_state==21`) is flagged as non-exclusive to that moment. 637 638Everything else in this document — game mode/module enum, the player-has-control rule, Link's position/state-machine fields, the full $7EF340.. inventory block (including the now fully-resolved pendant, crystal, and ability-flag bitfields), location fields, the complete 16-slot sprite and 10-slot ancilla tables with type-ID names, the dungeon room-flag SRAM mirror and its bit layout, the dungeon tile-attribute collision-grid address/formula/value table, dialog/text-box detection, and ROM identification — is verified against at least one primary source (usually two or three independent ones in exact agreement) and cited by file:line above. 639 640--- 641 642## 10. The way off the overworld — entrance and hole tables (2026-09-20) 643 644**A doorway is not a tile.** Nothing in the overworld attribute table marks 645one: `0x8E`/`0x8F` never occur outdoors at all (counted over the 512-entry 646`OverworldTileTypes` table in `spannerisms/usdasm` `bank_0E.asm`), and `0x8E` 647is an INDOOR attribute meaning "this tile takes Link out to the overworld" 648(zelda3 `src/dungeon.c:2106`, `:2149`, written by door objects at 649`src/dungeon.c:4152, 4166, 4211`). What makes a doorway is two table lookups 650on Link's POSITION, in `Overworld_UseEntrance` (`src/overworld.c:3217-3260`) 651and `LookupInOwEntranceTab2` (`:263-268`). 652 653| Table | SNES | Size | Extractor | 654|---|---|---|---| 655| `kOverworld_Entrance_Area` | `$9BB96F` | 129 words | zelda3 `assets/extract_resources.py:128`; usdasm `bank_1B.asm:392` | 656| `kOverworld_Entrance_Pos` | `$9BBA71` | 129 words | `:129`; usdasm `:525` | 657| `kOverworld_Entrance_Id` | `$9BBB73` | 129 bytes | `:130`; usdasm `:658` | 658| `kFallHole_Pos` | `$9BB800` | 19 words | `:138`; usdasm `bank_1B.asm:215` | 659| `kFallHole_Area` | `$9BB826` | 19 words | `:139`; usdasm `:236` | 660| `kFallHole_Entrances` | `$9BB84C` | 19 bytes | `:140`; usdasm `:257` | 661| `EntranceData.room_id` | `$02C813` | 134 words | usdasm `bank_02.asm:13125`, indexed by entrance id | 662 663`pos` is a map16 index inside the area: `pos = tile_y << 7 | tile_x << 1` 664(`assets/compile_resources.py:310`, taken apart at 665`assets/extract_resources.py:132`), so 666 667``` 668x = kOverworld_OffsetBaseX[area & 0x3F] + ((pos & 0x7E) >> 1) * 16 669y = kOverworld_OffsetBaseY[area & 0x3F] + (pos >> 7) * 16 670``` 671 672**The area matched is the HEAD of a big area** (`overworld_area_index`, 673`$7E040A`, set from `kOverworldAreaHeads`, `src/overworld.c:840`), not the 674512-grid quadrant in `$7E008A`. Matching on the quadrant finds nothing, and 675finds it silently. 676 677**The castle's secret-passage hole**, which the whole opening turns on: 678`kFallHole_Area[12] = $001B`, `kFallHole_Pos[12] = $0170`, 679`kFallHole_Entrances[12] = $7D` (usdasm `$9BB818` / `$9BB83E` / `$9BB858`) → 680tile 56 across, 2 down of the castle's big area → **world (2432, 1568)**, 681leading to **room `$0055`**. Cross-check: the castle's walk-in secret 682entrance (id `$32`, world (2240, 1744)) opens into the same room, and the 683exit table puts Link back beside the DOOR, not the hole - you fall in one way 684and walk out the other. 685 686### The overworld's area geometry 687 688`kOverworld_OffsetBaseX/Y[64]` (zelda3 `src/overworld.c:15-33`, ROM 689`$82A8C4`/`$82A944`) give the origin of the AREA a screen belongs to, so a 690quadrant of a big area returns the big area's corner. A big area is exactly 691four screens sharing an origin, which is how `packages/nav/src/world.rs` 692derives it rather than carrying `kOverworldMapIsSmall` as a second source that 693could disagree. `$7E070C` (`base_x`) is in EIGHTHS of a pixel-column while 694`$7E0708` (`base_y`) is in pixels (`src/overworld.c:868`), and masks are 695`0x3F0`/`0x7E` big, `0x1F0`/`0x3E` small (`:870-872`). 696 697Walking off an edge: `OverworldHandleTransitions` (`src/overworld.c:790-852`) 698adds ±1 or ±8 to the area index and then collapses it through 699`kOverworldAreaHeads`; a big area transitions only at its outer 1024-pixel 700boundary, never at an internal quadrant seam. 701 702### Liftables, named and gated 703 704`kTile50data` (`src/tile_detect.c:392`) orders the seven, and 705`kGetBestActionToPerformOnTile_a[i] - link_item_gloves <= 0` 706(`src/player.c:5496, 5531`) decides whether Link is strong enough. usdasm's 707id list at `$07DE70` and `LiftableGloveLevels` at `$07D375` name them and 708agree byte for byte: 709 710| Byte | What | Needs | 711|---|---|---| 712| `$50` | green bush | nothing | 713| `$51` | dark bush | nothing | 714| `$52` | small grey rock | Power Glove | 715| `$53` | small black rock | Titan's Mitt | 716| `$54` | sign | nothing | 717| `$55` | big grey rock | Power Glove | 718| `$56` | big black rock | Titan's Mitt | 719 720Pots are NOT in this range: a dungeon's liftables are `$70`-`$7F`, whose low 721nibble indexes `dung_replacement_tile_state`, translated back into this family 722by `kDungeon_QueryIfTileLiftable_rv` (`src/dungeon.c:125`). 723 724### Ledges are one-way, and the tile says which way 725 726`TileBehavior_Ledge_*`, `src/tile_detect.c:346-365`: `$28` north, `$29` 727south, `$2A`/`$2B` east-west (one shared case, so the tile alone does not say 728left from right - the engine takes the direction from which probe hit), 729`$2C`/`$2E` north-diagonal, `$2D`/`$2F` south-diagonal. The commonly cited 730alttp.run wiki split of `$2A`-`$2E` into four separate directions does not 731match the ROM. 732 733### Overworld holes 734 735Only `$20` occurs outdoors. `$B0`-`$BD` share the pit case in 736`TileDetect_ExecuteInner` but never appear in the overworld attribute table - 737they are the Cane of Somaria's platform tracks, and are dungeon-only. The pit 738handler reads no destination from the byte; the destination is 739`Overworld_GetPitDestination`'s position-plus-area lookup above. 740 741### Chests: opened or not 742 743A chest tile is `$58`-`$5D` (plus `$63`), and `tile - 0x58` is its slot. 744`dung_chest_locations[slot] & 0x8000` distinguishes a big-key LOCK from a 745chest (`src/tile_detect.c:413`, set at `src/dungeon.c:4043`). Whether it has 746been opened is bit `4 + slot` of the room's save word (§6.1, 747`kChestOpenMasks`, `src/dungeon.c:112`).