A Link to the Past (SNES, USA v1.0) — WRAM Map for an AI Harness
Sourced from cloned repositories (read-only research clones, never pushed/starred/forked):
| Repo | Local path | Role | Grade |
|---|---|---|---|
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. |
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. |
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. |
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. |
walkingeyerobot/alttp-disassembly | ~/src/github.com/walkingeyerobot/alttp-disassembly | MathOnNapkins' original disassembly. Stale since 2018. | Tertiary/stale cross-check only. |
| 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. |
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. |
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.
All addresses are given as $7Exxxx (WRAM bank $7E). zelda3's convention g_ram+0xNN is numerically 0xNN = the offset from $7E0000.
1. Game mode / submodule ($7E0010 / $7E0011)
| Address | Name (zelda3) | Cross-check name | Size | Citation |
|---|---|---|---|---|
$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) |
$7E0011 | submodule_index | SUBMODE | u8 | zelda3 src/variables.h:3; JaredBrian Other/symbols_wram.asm:83; jpdasm symbols_wram.asm:82 |
$7E00B0 | subsubmodule_index | — | u8 | zelda3 src/variables.h:122 |
$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 |
Module enum (main_module_index, values 0–27)
This 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.
| Value | Function name | Meaning |
|---|---|---|
| 0 | Module00_Intro | Title screen / attract sequence entry |
| 1 | Module01_FileSelect | File-select screen |
| 2 | Module02_CopyFile | Copy-file flow |
| 3 | Module03_KILLFile | Delete-file flow |
| 4 | Module04_NameFile | Name-entry (new file) |
| 5 | Module05_LoadFile | Loading a save into gameplay state |
| 6 | Module_PreDungeon | Pre-dungeon-entry transition/setup |
| 7 | Module07_Dungeon | Dungeon gameplay |
| 8 | Module08_OverworldLoad | Load an overworld area, light or dark world |
| 9 | Module09_Overworld | Overworld gameplay, light AND dark world — the world is $7EF3CA, not the module |
| 10 | Module08_OverworldLoad | Load a SPECIAL overworld area. usdasm names this slot Module0A_OverworldSpecialLoad (bank_00.asm:99); zelda3 reuses the function of 8 |
| 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 |
| 12 | Module_Unknown0 | Unused (assert(0) stub) — src/misc.c:203 |
| 13 | Module_Unknown1 | Unused (assert(0) stub) — src/misc.c:207 |
| 14 | Module0E_Interface | Dialog / menu / HUD-overlay module — see §7 for submodules |
| 15 | Module0F_SpotlightClose | Screen transition (spotlight closing) |
| 16 | Module10_SpotlightOpen | Screen transition (spotlight opening) |
| 17 | Module11_DungeonFallingEntrance | Falling into a dungeon entrance (pit-warp/entrance animation) |
| 18 | Module12_GameOver | Death sequence |
| 19 | Module13_BossVictory_Pendant | Pendant-boss victory sequence |
| 20 | Module14_Attract | Attract-mode demo playback |
| 21 | Module15_MirrorWarpFromAga | Agahnim mirror-warp cutscene |
| 22 | Module16_BossVictory_Crystal | Crystal-boss victory sequence |
| 23 | Module17_SaveAndQuit | Save-and-quit flow |
| 24 | Module18_GanonEmerges | Ganon-emerges cutscene |
| 25 | Module19_TriforceRoom | Ending / triforce room |
| 26 | Module1A_Credits | Credits |
| 27 | Module1B_SpawnSelect | Spawn-point select (used by save-file continue point selection) |
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).
Player-has-control vs. cutscene/transition/dialog — verified, exact rule
The engine names submodule 0 of both gameplay modules literally *_PlayerControl:
- Overworld:
Module09_00_PlayerControl— zelda3src/overworld.c:750 - Dungeon:
Module07_00_PlayerControl— zelda3src/dungeon.c:6594
Rule (from source, both functions gate identically):
player_has_control :=
(main_module_index == 7 or main_module_index in {9, 11})
and submodule_index == 0
and flag_custom_spell_anim_active == 0 ($7E0112)
and flag_is_link_immobilized == 0 ($7E02E4)
and flag_block_link_menu == 0 ($7E0FFC)
and (main_module_index == 7 or trigger_special_entrance == 0) ($7E04C6, overworld-only gate)
Citations: 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).
Any 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.
2. Link — position, direction, state machine, combat state
All 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".
| Address | Name | Size/type | Meaning | Citation |
|---|---|---|---|---|
$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 |
$7E0022 | link_x_coord | u16 | X position (pixel) | zelda3 variables.h:20; 3-way: JaredBrian :127 (POSX), jpdasm :127 |
$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 |
$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 |
$7E0476 | link_is_on_lower_level_mirror | u8 | Mirror/cached copy of the above, read by sprite code | zelda3 variables.h:463 |
$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 |
$7E0066 | link_last_direction_moved_towards | u8 | Last direction actually moved | zelda3 variables.h:87 |
$7E0067 | link_direction | u8 | Current movement direction (distinct from facing) | zelda3 variables.h:88 |
$7E0026 | link_direction_last | u8 | Previous facing/direction snapshot | zelda3 variables.h:22 |
$7E0027 | link_actual_vel_y | i8 | Actual Y velocity | zelda3 variables.h:23 |
$7E0028 | link_actual_vel_x | i8 | Actual X velocity | zelda3 variables.h:24 |
$7E0030 | link_y_vel | u8 | Y velocity (intended/nominal) | zelda3 variables.h:31 |
$7E0031 | link_x_vel | u8 | X velocity (intended/nominal) | zelda3 variables.h:32 |
$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 |
$7E005E | link_speed_setting | u8 | Speed tier setting | zelda3 variables.h:72 |
$7E0057 | link_speed_modifier | u8 | Speed modifier (e.g. Pegasus Boots dash multiplier) | zelda3 variables.h:63 |
$7E0056 | link_is_bunny | u8 (bool) | 1 = currently a bunny (Moon Pearl missing, in dark world) | zelda3 variables.h:62 |
$7E0055 | link_cape_mode | u8 | Cape-active state | zelda3 variables.h:61 |
$7E0046 | link_incapacitated_timer | u8 | Frames Link is incapacitated (knocked back / stunned) | zelda3 variables.h:50 |
$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 |
$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++) |
$7E0048 | bitmask_of_dragstate | u8, bitfield | Dash/drag state bitmask | zelda3 variables.h:52 |
$7E004D | link_auxiliary_state | u8 | Auxiliary state (used with visibility/pose fields) | zelda3 variables.h:56 |
$7E004B | link_visibility_status | u8 | Visibility status (e.g. hidden during warps) | zelda3 variables.h:54 |
$7E02DA | link_pose_for_item | u8 | Item-use pose flag | zelda3 variables.h:238 |
$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 |
$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 |
$7E0202 | hud_cur_item | u8 | Cursor index into the inventory grid (used when choosing a new Y item) | zelda3 variables.h:189 |
$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 (`... |
$7E003D | link_delay_timer_spin_attack | u8 | Spin-attack delay timer | zelda3 variables.h:41 |
$7E0079 | link_spin_attack_step_counter | u8 | Spin-attack animation step counter | zelda3 variables.h:91 |
$7E031C | state_for_spin_attack | u8 | Spin-attack sub-state | zelda3 variables.h:288 |
$7E031D | step_counter_for_spin_attack | u8 | (second) spin-attack step counter | zelda3 variables.h:289 |
$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 |
$7E02F4 | flag_is_sprite_to_pick_up_cached | u8 | Cached copy of the above | zelda3 variables.h:260 |
$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 |
$7E037A | link_position_mode | u8 | Position-lock mode (e.g. during boomerang throw) | zelda3 variables.h:350 |
Link state machine enum (link_player_handler_state, $7E005D)
Complete 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.
| Value | Name | Meaning |
|---|---|---|
| 0 | kPlayerState_Ground | Normal ground movement |
| 1 | kPlayerState_FallingIntoHole | Falling into a pit |
| 2 | kPlayerState_RecoilWall | Recoiling off a wall |
| 3 | kPlayerState_SpinAttacking | Spin attack in progress |
| 4 | kPlayerState_Swimming | Swimming |
| 5 | kPlayerState_TurtleRock | Turtle Rock ice-physics state |
| 6 | kPlayerState_RecoilOther | Recoiling (other cause) |
| 7 | kPlayerState_Electrocution | Being electrocuted |
| 8 | kPlayerState_Ether | Casting Ether medallion |
| 9 | kPlayerState_Bombos | Casting Bombos medallion |
| 10 | kPlayerState_Quake | Casting Quake medallion |
| 12 | kPlayerState_FallOfLeftRightLedge | Falling off a side ledge |
| 14 | kPlayerState_JumpOffLedgeDiag | Jumping off a diagonal ledge |
| 17 | kPlayerState_StartDash | Dash starting (Pegasus Boots) |
| 18 | kPlayerState_StopDash | Dash stopping |
| 19 | kPlayerState_Hookshot | Hookshot travel |
| 20 | kPlayerState_Mirror | Using the Magic Mirror |
| 21 | kPlayerState_HoldUpItem | Holding up an item (item-get pose) — also the general "lifted object overhead" pose |
| 22 | kPlayerState_AsleepInBed | Asleep in bed (game start) |
| 23 | kPlayerState_PermaBunny | Permanently a bunny (no Moon Pearl, in a bunny-forcing area) |
| 25 | kPlayerState_ReceivingEther | Receiving the Ether medallion cutscene |
| 26 | kPlayerState_ReceivingBombos | Receiving the Bombos medallion cutscene |
| 27 | kPlayerState_OpeningDesertPalace | Desert Palace opening-prayer cutscene |
| 28 | kPlayerState_TempBunny | Temporarily a bunny |
| 29 | kPlayerState_PullForRupees | Pulling a "grab for rupees" object |
| 30 | kPlayerState_SpinAttackMotion | Spin attack motion (distinct from state 3) |
"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).
3. Health, magic, currency, and the full inventory block ($7EF340..)
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.
| Address | Name | Size | Meaning | Citation |
|---|---|---|---|---|
$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 |
$7EF341 | link_item_boomerang | u8 | 0=none, 1=Blue, 2=Red (conventional; only nonzero-ness confirmed in source) | zelda3 variables.h:1065 |
$7EF342 | link_item_hookshot | u8 (bool) | Hookshot owned | zelda3 variables.h:1066 |
$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 |
$7EF344 | link_item_mushroom | u8 | Mushroom/Powder item state | zelda3 variables.h:1068 |
$7EF345 | link_item_fire_rod | u8 (bool) | Fire Rod owned | zelda3 variables.h:1069 |
$7EF346 | link_item_ice_rod | u8 (bool) | Ice Rod owned | zelda3 variables.h:1070 |
$7EF347 | link_item_bombos_medallion | u8 (bool) | Bombos owned | zelda3 variables.h:1071 |
$7EF348 | link_item_ether_medallion | u8 (bool) | Ether owned | zelda3 variables.h:1072 |
$7EF349 | link_item_quake_medallion | u8 (bool) | Quake owned | zelda3 variables.h:1073 |
$7EF34A | link_item_torch | u8 (bool) | Fire rod's "lit torch" carry state / lantern-lit flag | zelda3 variables.h:1074 |
$7EF34B | link_item_hammer | u8 (bool) | Hammer owned | zelda3 variables.h:1075 |
$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 |
$7EF34D | link_item_bug_net | u8 (bool) | Bug net owned | zelda3 variables.h:1077 |
$7EF34E | link_item_book_of_mudora | u8 (bool) | Book of Mudora owned | zelda3 variables.h:1078 |
$7EF34F | link_item_bottle_index | u8 | Index/count related to bottle slots | zelda3 variables.h:1079 |
$7EF350 | link_item_cane_somaria | u8 (bool) | Cane of Somaria owned | zelda3 variables.h:1080 |
$7EF351 | link_item_cane_byrna | u8 (bool) | Cane of Byrna owned | zelda3 variables.h:1081 |
$7EF352 | link_item_cape | u8 (bool) | Magic Cape owned | zelda3 variables.h:1082 |
$7EF353 | link_item_mirror | u8 (bool) | Magic Mirror owned | zelda3 variables.h:1083 |
$7EF354 | link_item_gloves | u8 | 0=none, 1=Power Glove, 2=Titan's Mitt (conventional; only presence directly confirmed) | zelda3 variables.h:1084 |
$7EF355 | link_item_boots | u8 (bool) | Pegasus Boots owned | zelda3 variables.h:1085 |
$7EF356 | link_item_flippers | u8 (bool) | Flippers owned | zelda3 variables.h:1086 |
$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 |
$7EF359 | link_sword_type | u8 | See §2 combat table | zelda3 variables.h:1088 |
$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 |
$7EF35B | link_armor | u8 | Tunic/armor level (0=Green,1=Blue,2=Red conventional) | zelda3 variables.h:1090 |
$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 |
$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 |
$7EF362 | link_rupees_actual | u16 | The real spendable rupee count | zelda3 variables.h:1093 |
$7EF364 | link_compass | u16, bitfield | Compass-owned bitfield (bit per dungeon) — layout below | zelda3 variables.h:1094 |
$7EF366 | link_bigkey | u16, bitfield | Big-key-owned bitfield, identical bit layout to compass | zelda3 variables.h:1095 |
$7EF368 | link_dungeon_map | u16, bitfield | Dungeon-map-owned bitfield, identical bit layout to compass | zelda3 variables.h:1096 |
$7EF36A | link_rupees_in_pond | u8 | Wishing-pond rupee toss counter | zelda3 variables.h:1097 |
$7EF36B | link_heart_pieces | u8 | Heart-piece count, 0-3 (4th piece rolls into capacity) | zelda3 variables.h:1098 |
$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 |
$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 |
$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 |
$7EF36F | link_num_keys | u8 | Current dungeon's small-key count | zelda3 variables.h:1102 |
$7EF370 | link_bomb_upgrades | u8 | Bomb-capacity upgrade count | zelda3 variables.h:1103 |
$7EF371 | link_arrow_upgrades | u8 | Arrow-capacity upgrade count | zelda3 variables.h:1104 |
$7EF372 | link_hearts_filler | u8 | Sub-heart HP fill accumulator (animates HP regen) | zelda3 variables.h:1105 |
$7EF373 | link_magic_filler | u8 | Sub-unit magic fill accumulator | zelda3 variables.h:1106 |
$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 |
$7EF375 | link_bomb_filler | u8 | Sub-unit bomb fill accumulator | zelda3 variables.h:1108 |
$7EF376 | link_arrow_filler | u8 | Sub-unit arrow fill accumulator | zelda3 variables.h:1109 |
$7EF377 | link_num_arrows | u8 | Current arrow count | zelda3 variables.h:1110 |
$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 |
$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 |
$7EF37B | link_magic_consumption | u8 | Magic-drain-rate modifier (e.g. halved by 1/2 Magic upgrade) | zelda3 variables.h:1113 |
$7EF37C.. | link_keys_earned_per_dungeon[] | u8 array | Per-dungeon key-earned counters (array, not indexed range confirmed beyond base) | zelda3 variables.h:1114 |
$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 | =0x8000two lines above it is the *same* boss-kill event as §6.1's bit 11); dozens of<2/>=2/<3guards throughoutoverworld.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 |
$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 |
$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 |
$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 |
$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 |
$7EF3CA | savegame_is_darkworld | u8 (bool) | World flag: 0 = Light World, nonzero = Dark World | zelda3 variables.h:1120 |
$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 |
$7EF3CD/$7EF3CF | saved_tagalong_y / saved_tagalong_x | u16 each | Saved follower position | zelda3 variables.h:1122-1123 |
$7EF3D1 | saved_tagalong_indoors | u8 | Follower's saved indoors flag | zelda3 variables.h:1124 |
$7EF3D2 | saved_tagalong_floor | u8 | Follower's saved floor/layer | zelda3 variables.h:1125 |
$7EF3D3 | follower_dropped | u8 (bool) | Whether the follower was dropped | zelda3 variables.h:1126 |
$7EF3E7 | deaths_per_palace | u16 array | Death counter per palace | zelda3 variables.h:1127 |
$7EF33F | byte_7EF33F | u8 | Unnamed byte immediately before the item block — likely padding/unused | zelda3 variables.h:1063 |
$7EF300 | savegame_has_master_sword_flags | u16 | Master-Sword-related flags | zelda3 variables.h:1062 |
$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 |
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):
SET 2 SET 1
xced aspm wihb tg..
- SET 1 (low byte, e.g.
$7EF364for compass):0x80=SkullWoods(w),0x40=IcePalace(i),0x20=TowerOfHera(h),0x10=ThievesTown(b),0x08=TurtleRock(t),0x04=Ganon'sTower(g); low 2 bits unused - SET 2 (high byte, e.g.
$7EF365for 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)
Applies identically to link_bigkey ($7EF366/367) and link_dungeon_map ($7EF368/369). Cite: jpdasm/symbols_sram.asm:719-736.
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.
4. Location
| Address | Name | Size | Meaning | Citation |
|---|---|---|---|---|
$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 |
$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) |
$7E00A0 | dungeon_room_index | u16 | Dungeon/indoor room id | zelda3 variables.h:112; 3-way: JaredBrian :544 (ROOM), jpdasm :539 |
$7E00A2 | dungeon_room_index_prev | u16 | Previous room id (for detecting room transitions) | zelda3 variables.h:113 |
$7E048E | dungeon_room_index2 | u16 | A second/cached room-index copy | zelda3 variables.h:475 |
$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) |
$7EF3CA | savegame_is_darkworld | u8 (bool) | World: 0 = Light World, nonzero = Dark World (see §3) | zelda3 variables.h:1120 |
$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 |
$7E0618/$7E061A | camera_y_coord_scroll_low/_hi | u16 each | Camera Y scroll position | zelda3 variables.h:531-532 |
$7E061C/$7E061E | camera_x_coord_scroll_low/_hi | u16 each | Camera X scroll position | zelda3 variables.h:533-534 |
$7E00EE | link_is_on_lower_level | u8 | Layer/floor within the current room (see §2) | zelda3 variables.h:141 |
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.
5. Sprites, enemies, and ancillae (projectiles)
Researched 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).
5.1 The 16-slot sprite tables
All 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.)
| Address | Name | Citation | Meaning |
|---|---|---|---|
| $7E0B58 | sprite_stunned | variables.h:656 | Auto-decrementing stun timer |
| $7E0B6B | sprite_flags | variables.h:660 | Bitflags incl. tile-hitbox / deflect-arrows / boss-death bits (secondary AsarUSA symbols_wram.asm:3190) |
| $7E0B89 | sprite_obj_prio | variables.h:666 | OAM object priority; set in Sprite_TimersAndOam, src/sprite.c:1190,1204 |
| $7E0BA0 | sprite_ignore_projectile | variables.h:672 | "Bulletproof" — nonzero means ancillae/projectiles don't interact with this sprite |
| $7E0BB0 | sprite_unk2 | variables.h:673 | Stores the ancilla ID that last hit this sprite (secondary AsarUSA:3298) |
| $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) |
| $7E0BE0 | sprite_flags5 | variables.h:675 | Bitflags incl. tile-interaction/shield-block/damage-SFX/prize-pack (secondary AsarUSA:3376) |
| $7E0C9A | sprite_room | variables.h:693 | Overworld screen/room the sprite belongs to (kills sprite on screen transition) |
| $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) |
| $7E0CBA | sprite_die_action | variables.h:695 | Forced drop on death (secondary AsarUSA:3656: 0=nothing,1=key,2=big key,3=green rupee) |
| $7E0CD2 | sprite_bump_damage | variables.h:697 | Contact damage dealt to Link on touch |
| $7E0CE2 | sprite_give_damage | variables.h:698 | Damage being dealt TO this sprite this frame; subtracted from sprite_health, src/sprite.c:2409-2413 |
| $7E0D00 | sprite_y_lo | variables.h:711 | Y position, low byte |
| $7E0D10 | sprite_x_lo | variables.h:712 | X position, low byte |
| $7E0D20 | sprite_y_hi | variables.h:713 | Y position, high byte |
| $7E0D30 | sprite_x_hi | variables.h:714 | X position, high byte; combined via Sprite_Get16BitCoords, src/sprite.c:1207-1210 |
| $7E0D40 | sprite_y_vel | variables.h:715 | Y velocity |
| $7E0D50 | sprite_x_vel | variables.h:716 | X velocity |
| $7E0D60 | sprite_y_subpixel | variables.h:717 | Y sub-pixel accumulator |
| $7E0D70 | sprite_x_subpixel | variables.h:718 | X sub-pixel accumulator |
| $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 |
| $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) |
| $7E0DC0 | sprite_graphics | variables.h:723 | Graphics/animation-frame control |
| $7E0DD0 | sprite_state | variables.h:724 | Top-level lifecycle dispatch index — see 5.2/5.3 |
| $7E0DF0/0E00/0E10 | sprite_delay_main/_aux1/_aux2 | variables.h:726-728 | Auto-decrementing timers, src/sprite.c:1171-1176 |
| $7E0E20 | sprite_type | variables.h:729 | Sprite type ID (0x00-0xF2) — dispatch index into kSpriteActiveRoutines[243], see 5.4 |
| $7E0E30 | sprite_subtype | variables.h:730 | Per-type auxiliary subtype byte |
| $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 |
| $7E0E50 | sprite_health | variables.h:732 | Generic hit-point counter — see 5.3 |
| $7E0E60 | sprite_flags3 | variables.h:733 | Bitflags: death-anim/impervious/shadow-size/has-shadow/OAM-palette/OAM-nametable |
| $7E0E70 | sprite_wallcoll | variables.h:734 | Wall-collision-related flags |
| $7E0EB0 | sprite_head_dir | variables.h:738 | Facing/movement direction |
| $7E0EC0 | sprite_anim_clock | variables.h:739 | Animation clock |
| $7E0EF0 | sprite_hit_timer | variables.h:742 | Hit/knockback-recoil timer; high bit = "recoiling" flag, src/sprite.c:1180-1204 |
| $7E0F00 | sprite_pause | variables.h:743 | Per-sprite pause flag, src/sprite.c:1493-1498 |
| $7E0F20 | sprite_floor | variables.h:745 | Floor/layer (0/1, BG1 vs BG2) |
| $7E0F30/0F40 | sprite_y_recoil/sprite_x_recoil | variables.h:746-747 | Knockback/recoil offsets |
| $7E0F50 | sprite_oam_flags | variables.h:748 | OAM attribute flags actually copied to hardware OAM |
| $7E0F60 | sprite_flags4 | variables.h:749 | Bitflags: ignore-collision / stasis (doesn't count toward room-clear) / activeness / hitbox ID |
| $7E0F70/0F80/0F90 | sprite_z/sprite_z_vel/sprite_z_subpos | variables.h:750-752 | Height/jump coordinate, velocity, sub-position |
Two 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.
A 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.
5.2 Alive/active detection — verified from the dispatcher itself
// src/sprite.c:1212-1217
void Sprite_ExecuteSingle(int k) {
uint8 st = sprite_state[k];
if (st != 0)
Sprite_TimersAndOam(k);
kSprite_ExecuteSingle[st](k);
}
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).
5.3 Three-tier alive/dead/never-spawned — verified, matches the harness's needs exactly
Beyond the per-frame sprite_state, there is a persistent per-room permanent-kill bitmask, separate from any live slot:
// src/sprite.c:3632-3636
void Sprite_ManuallySetDeathFlagUW(int k) {
if (!player_is_indoors || sprite_defl_bits[k] & 1 || sign8(sprite_N[k]))
return;
sprite_where_in_room[dungeon_room_index2] |= 1 << sprite_N[k];
}
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).
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.
5.4 Sprite type ID → name (verified, primary, exhaustively indexed)
Dispatch 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).
| ID | Name | Notes |
|---|---|---|
| $00 | Raven | |
| $01 | Vulture | |
| $02 | StalfosHead | |
| $08, $0A | Octorok (2 color variants, shared handler) | |
| $09 | GiantMoldorm | |
| $0B | Cucco | |
| $0C | OctorokStone | |
| $0D | Buzzblob | |
| $11 | Hinox | |
| $12 | Moblin | |
| $41-$43 | BlueGuard (×3) | soldier variant family |
| $44 | BluesainBolt | soldier variant ("UsainBolt" in jpdasm — cosmetic naming difference only) |
| $45 | HogSpearMan | soldier variant |
| $46 | BlueArcher | soldier variant |
| $47 | GreenBushGuard | soldier variant |
| $48 | RedJavelinGuard | soldier variant |
| $49 | RedBushGuard | soldier variant |
| $4A | BombGuard | soldier variant |
| $4B | GreenKnifeGuard | soldier variant |
| $4C | Geldman | |
| $4D | Toppo | |
| $6D | Rat | |
| $6E | Rope | |
| $6F | Keese (bat) | |
| $73 | Uncle and Priest | |
| $76 | Zelda | |
| $78 | Mrs. Sahasrahla | |
| $7A | Agahnim | |
| $B4 | Purple Chest | the only sprite-table chest (roaming quest item) — see note below on ordinary chests |
| $C7 | Pokey | |
| $D8 | Heart drop | |
| $D9-$DB | Rupee drop (Green/Blue/Red, per jpdasm cross-check) | |
| $E4-$E5 | Key drop (Small Key / Big Key, per jpdasm cross-check) | |
| $EA | Heart Container | |
| $EB | Heart Piece |
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).
5.5 Ancilla (projectile) table — verified, primary, exhaustive
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:
| Address | Name | Citation | Meaning |
|---|---|---|---|
| $7E0BFA | ancilla_y_lo | variables.h:677 | Y position low byte |
| $7E0C04 | ancilla_x_lo | variables.h:678 | X position low byte |
| $7E0C0E | ancilla_y_hi | variables.h:679 | Y position high byte |
| $7E0C18 | ancilla_x_hi | variables.h:680 | X position high byte |
| $7E0C22 | ancilla_y_vel | variables.h:681 | Y velocity |
| $7E0C2C | ancilla_x_vel | variables.h:682 | X velocity |
| $7E0C4A | ancilla_type | variables.h:685 | Type ID, 1-based (0=empty slot) — dispatch src/ancilla.c:661-677 |
| $7E0C54 | ancilla_step | variables.h:686 | Per-type step/sub-state scratch |
| $7E0C5E | ancilla_item_to_link | variables.h:687 | Tracks hookshot extension / item-receipt ID (secondary AsarUSA:3534) |
| $7E0C68 | ancilla_timer | variables.h:688 | General auto-decrementing timer, src/ancilla.c:674-675 |
| $7E0C72 | ancilla_dir | variables.h:689 | Direction |
| $7E0C7C | ancilla_floor | variables.h:690 | Floor/layer (0/1), src/ancilla.c:764-766 |
| $7E0C90 | ancilla_numspr | variables.h:692 | Number of OAM sprites this ancilla uses |
5.6 Ancilla type IDs — complete, from kAncilla_Funcs[67] (src/ancilla.c:252-320, 1-indexed via ancilla_type-1)
| ID | Meaning | ID | Meaning |
|---|---|---|---|
| 1 | Somaria bullet | 2 | Fire Rod shot |
| 4 | Sword-beam impact | 5 | Boomerang |
| 6 | Generic wall-hit | 7 | Bomb |
| 8 | Door debris | 9 | Arrow |
| 10 | Arrow stuck in wall | 11 | Ice Rod shot |
| 12 | Sword beam (full-health charge) | 13 | Full-charge spin spark |
| 17 | Ice Rod wall-hit | 19 | Ice Rod sparkle |
| 21 | Jump/water splash | 22 | Hit-stars stun effect |
| 24 | Ether medallion | 25 | Bombos medallion |
| 26 | Magic Powder dust | 28 | Quake medallion |
| 31 | Hookshot | 34 | Item-get animation |
| 40 | Wishing-pond item toss | 49 | Cane of Byrna shield spark |
| 51 | Blast-wall explosion | 53 | Master Sword receipt |
| 54 | Flute effect | 58 | Big Bomb explosion |
| 63 | Bush destruction poof | 67 | Ganon's Tower cutscene effect |
(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.)
6. Dungeon room state and tile-attribute/collision maps
Researched 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.
6.1 SRAM room-flags mirror ($7EF000)
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.
Bit 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:
bit: 15 14 13 12 | 11 10 | 9 8 7 6 5 4 | 3 2 1 0
d d d d | b k | u t s e h c | q q q q
(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.)
- bits 0-3 (
qqqq) = quadrant-visited flags (dung_quadrants_visited, live at $7E0408) - 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 inOpenChestForItem(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" fort, "chest 5 / 2nd key / heart piece" foru). 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. - 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 - bit 11 (
b) = boss defeated / heart container taken — one single consistent meaning, pre-shift bit 0x8000, set inPrepareDungeonExitFromBossFight(src/dungeon.c:2497), read for crystal/pendant rewards (src/dungeon.c:4584,4596) - bits 12-15 (
dddd) =dung_door_opened— up to 4 unlockable/breakable doors in the room (live at $7E0400)
Live 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).
6.2 Current room/dungeon ID — confirmed exactly, with one nuance
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.
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.
6.3 Dungeon tile attribute / collision map — the core walkability-grid mechanism
Base addresses, both independently confirmed (jpdasm symbols_wram.asm:6002-6003, usdasm bank files):
dung_bg2_attr_table= $7F2000 (variables.h:1161)dung_bg1_attr_table= $7F3000 (variables.h:1162)
(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.)
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).
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.)
Pixel → index formula (exact, from TileDetection_Execute, src/tile_detect.c:240-254):
index = (pixelY >> 3) * 64 + (pixelX >> 3) // row-major, 64-wide
addr = $7F2000 + index + (link_is_on_lower_level ? 0x1000 : 0)
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:
column = ((x & 0x1F8) >> 3) // 0..63
row = ((y & 0x1F8) >> 3) // 0..63
index = row * 64 + column
Measured 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.
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.
link_is_on_lower_level ($7E00EE, §2) is exactly the flag selecting the $7F2000 vs. $7F3000 half.
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:
| Value(s) | Meaning |
|---|---|
0x00 | Standard floor (walkable) |
0x01-0x04, 0x26, 0x43 | Solid / wall collision |
0x08 | Deep water |
0x09 | Shallow water |
0x0D | Spike floor |
0x0E/0x0F | Ice (GT / Ice Palace) |
0x10-0x13, 0x18-0x1B | Diagonal slopes (4 orientations ×2 variants) |
0x1D-0x1F, 0x3D-0x3F | Auto-stairs / layer-swap (N/S) |
0x20, 0xB0-0xBD | Pit / Somaria-platform-pit variants |
0x22, 0x30-0x39 | Manual and straight inter-room stairs |
0x28-0x2F | Ledges (N/S/E/W + diagonal) |
0x44 | Spike |
0x46 | Desert Palace tablet trigger |
0x50-0x56 | Liftable objects (bush/rock/sign variants) |
0x58-0x5D | Chest 0-5 (value directly encodes which chest slot, matching kChestOpenMasks index, §6.1) |
0x5E-0x5F | Spiral stairs |
0x60 | Rupee tile |
0x63 | Minigame chest |
0x67-0x6B | Crystal peg / conveyors |
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) |
0xC0-0xCF | Lightable torches |
0xD0-0xFF | Mostly unused; 0xF0-0xFF zelda3 labels TileBehavior_FlaggableDoor |
6.4 Overworld tile attribute / collision — structurally DIFFERENT from dungeons, verified
There 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.
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:
| Table | SNES address | Size | zelda3's own extractor |
|---|---|---|---|
kMap16ToMap8 | $8F8000 | 3752 × 4 words | assets/compile_resources.py:155 |
kMap8DataToTileAttr | $8E9459 | 512 bytes | assets/compile_resources.py:436 |
Both 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.
The 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).
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.
6.5 Door/stairs tables — ROM-resident per room, not a persistent WRAM table (verified, not assumed)
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.
7. Dialog / text box / menu detection
| Address | Name | Size | Meaning | Citation |
|---|---|---|---|---|
$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 |
$7E1CD8 | messaging_module | u8 | Sub-mode within the messaging system | zelda3 variables.h:820 |
$7E1CE0 | text_wait_countdown | u16 | Generic text timing countdown | zelda3 variables.h:825 |
$7E1CE8 | choice_in_multiselect_box | u8 | Which option is highlighted in a 2/3-choice text prompt | zelda3 variables.h:828 |
$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 |
$7E1CF0 | dialogue_message_index | u16 | Which dialogue message is loaded/showing | zelda3 variables.h:833 |
Text box open/closed — exact rule, verified from source:
text_box_open := (main_module_index == 14) and (submodule_index == 2)
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."
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.
Menu/pause-related submodules of module 14 (table kMessagingSubmodules[12], src/messaging.c:184-196, dispatch src/messaging.c:308-310 kMessagingSubmodules[submodule_index]()):
submodule_index | Function | Meaning |
|---|---|---|
| 0 | Module_Messaging_0 | Unused stub (assert(0)) |
| 1 | Hud_Module_Run | HUD update pass |
| 2 | RenderText | Text box actively rendering (see rule above) |
| 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) |
| 4 | Module0E_04_RedPotion | Drinking a red potion (HP refill animation) |
| 5 | Module0E_05_DesertPrayer | Desert Palace opening-prayer cutscene |
| 6 | Module_Messaging_6 | Unused stub |
| 7 | Messaging_OverworldMap | World map screen (opened via Select/map button on the overworld, src/overworld.c:760-763) |
| 8 | Module0E_08_GreenPotion | Drinking a green potion (magic refill) |
| 9 | Module0E_09_BluePotion | Drinking a blue potion (both refill) |
| 10 | Module0E_0A_FluteMenu | Flute-warp menu |
| 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) |
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).
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.
8. Intro sequence (file select → rescuing Zelda)
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.
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).
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.
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.
9. ROM identification (verified: zelda3 source code + spannerisms/usdasm README + web search attributed to No-Intro — three independent agreements)
| Property | Value | Source |
|---|---|---|
| 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) |
| Headered file size | 1,049,088 bytes (0x100200) — unheadered size + 512-byte copier header | zelda3 assets/util.py:63-65 (same check) |
| 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:] |
| 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) |
| USA v1.0 MD5 (unheadered) | 608C22B8FF930C62DC2DE54BCD6EBA72 | spannerisms/usdasm README.md (Binaries section); matches the No-Intro-attributed web search result |
| USA v1.0 CRC32 (unheadered) | 777AAC2F | spannerisms/usdasm README.md; matches the No-Intro-attributed web search result |
| 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. |
| 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 |
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.
Open items — not resolved in this pass, do not treat as answered
Exact direction-value mapping for— RESOLVED 2026-09-20, from source, not empirically.link_direction_facing($7E002F)0 = up, 2 = down, 4 = left, 6 = right. Two independent places in zelda3 pin it:kDashTab2[] = {8, 4, 2, 1}is indexed bylink_direction_facing >> 1and 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); andsrc/misc.c:706-708setslink_direction = 8(up) andlink_direction_facing = 0in 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 asalttp::Direction::facing.- 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'srooms.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. - Door/staircase entry bit-packing —
dung_door_tilemap_address($7E19A0) anddung_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. - 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. - A handful of obscure dungeon door sub-type tile values (
0x88,0x8A-0x8D, most of0x90-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. - Some
sprite_unk3/unk4/unk5-style scratch fields (variables.h:1216-1221) and a fewalt_sprite_*cache fields in the$7E1FA6C-$7E1FADCrange (§5.1) — undocumented in all four cloned repos, not guessed at. - Whether every soldier-guard sprite handler uses only the generic
sprite_healthdamage path (§5.3) — no bespoke HP override was found in the handlers read, but not every soldier function body was read exhaustively. - 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.
Everything 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.
10. The way off the overworld — entrance and hole tables (2026-09-20)
A doorway is not a tile. Nothing in the overworld attribute table marks
one: 0x8E/0x8F never occur outdoors at all (counted over the 512-entry
OverworldTileTypes table in spannerisms/usdasm bank_0E.asm), and 0x8E
is an INDOOR attribute meaning "this tile takes Link out to the overworld"
(zelda3 src/dungeon.c:2106, :2149, written by door objects at
src/dungeon.c:4152, 4166, 4211). What makes a doorway is two table lookups
on Link's POSITION, in Overworld_UseEntrance (src/overworld.c:3217-3260)
and LookupInOwEntranceTab2 (:263-268).
| Table | SNES | Size | Extractor |
|---|---|---|---|
kOverworld_Entrance_Area | $9BB96F | 129 words | zelda3 assets/extract_resources.py:128; usdasm bank_1B.asm:392 |
kOverworld_Entrance_Pos | $9BBA71 | 129 words | :129; usdasm :525 |
kOverworld_Entrance_Id | $9BBB73 | 129 bytes | :130; usdasm :658 |
kFallHole_Pos | $9BB800 | 19 words | :138; usdasm bank_1B.asm:215 |
kFallHole_Area | $9BB826 | 19 words | :139; usdasm :236 |
kFallHole_Entrances | $9BB84C | 19 bytes | :140; usdasm :257 |
EntranceData.room_id | $02C813 | 134 words | usdasm bank_02.asm:13125, indexed by entrance id |
pos is a map16 index inside the area: pos = tile_y << 7 | tile_x << 1
(assets/compile_resources.py:310, taken apart at
assets/extract_resources.py:132), so
x = kOverworld_OffsetBaseX[area & 0x3F] + ((pos & 0x7E) >> 1) * 16
y = kOverworld_OffsetBaseY[area & 0x3F] + (pos >> 7) * 16
The area matched is the HEAD of a big area (overworld_area_index,
$7E040A, set from kOverworldAreaHeads, src/overworld.c:840), not the
512-grid quadrant in $7E008A. Matching on the quadrant finds nothing, and
finds it silently.
The castle's secret-passage hole, which the whole opening turns on:
kFallHole_Area[12] = $001B, kFallHole_Pos[12] = $0170,
kFallHole_Entrances[12] = $7D (usdasm $9BB818 / $9BB83E / $9BB858) →
tile 56 across, 2 down of the castle's big area → world (2432, 1568),
leading to room $0055. Cross-check: the castle's walk-in secret
entrance (id $32, world (2240, 1744)) opens into the same room, and the
exit table puts Link back beside the DOOR, not the hole - you fall in one way
and walk out the other.
The overworld's area geometry
kOverworld_OffsetBaseX/Y[64] (zelda3 src/overworld.c:15-33, ROM
$82A8C4/$82A944) give the origin of the AREA a screen belongs to, so a
quadrant of a big area returns the big area's corner. A big area is exactly
four screens sharing an origin, which is how packages/nav/src/world.rs
derives it rather than carrying kOverworldMapIsSmall as a second source that
could disagree. $7E070C (base_x) is in EIGHTHS of a pixel-column while
$7E0708 (base_y) is in pixels (src/overworld.c:868), and masks are
0x3F0/0x7E big, 0x1F0/0x3E small (:870-872).
Walking off an edge: OverworldHandleTransitions (src/overworld.c:790-852)
adds ±1 or ±8 to the area index and then collapses it through
kOverworldAreaHeads; a big area transitions only at its outer 1024-pixel
boundary, never at an internal quadrant seam.
Liftables, named and gated
kTile50data (src/tile_detect.c:392) orders the seven, and
kGetBestActionToPerformOnTile_a[i] - link_item_gloves <= 0
(src/player.c:5496, 5531) decides whether Link is strong enough. usdasm's
id list at $07DE70 and LiftableGloveLevels at $07D375 name them and
agree byte for byte:
| Byte | What | Needs |
|---|---|---|
$50 | green bush | nothing |
$51 | dark bush | nothing |
$52 | small grey rock | Power Glove |
$53 | small black rock | Titan's Mitt |
$54 | sign | nothing |
$55 | big grey rock | Power Glove |
$56 | big black rock | Titan's Mitt |
Pots are NOT in this range: a dungeon's liftables are $70-$7F, whose low
nibble indexes dung_replacement_tile_state, translated back into this family
by kDungeon_QueryIfTileLiftable_rv (src/dungeon.c:125).
Ledges are one-way, and the tile says which way
TileBehavior_Ledge_*, src/tile_detect.c:346-365: $28 north, $29
south, $2A/$2B east-west (one shared case, so the tile alone does not say
left from right - the engine takes the direction from which probe hit),
$2C/$2E north-diagonal, $2D/$2F south-diagonal. The commonly cited
alttp.run wiki split of $2A-$2E into four separate directions does not
match the ROM.
Overworld holes
Only $20 occurs outdoors. $B0-$BD share the pit case in
TileDetect_ExecuteInner but never appear in the overworld attribute table -
they are the Cane of Somaria's platform tracks, and are dungeon-only. The pit
handler reads no destination from the byte; the destination is
Overworld_GetPitDestination's position-plus-area lookup above.
Chests: opened or not
A chest tile is $58-$5D (plus $63), and tile - 0x58 is its slot.
dung_chest_locations[slot] & 0x8000 distinguishes a big-key LOCK from a
chest (src/tile_detect.c:413, set at src/dungeon.c:4043). Whether it has
been opened is bit 4 + slot of the room's save word (§6.1,
kChestOpenMasks, src/dungeon.c:112).