jevsnes.git / research / alttp-ram-map.md

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):

RepoLocal pathRoleGrade
snesrev/zelda3~/src/github.com/snesrev/zelda3C 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/usdasmFull 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/AsarUSALTTPDisassemblyUSA 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/jpdasmJP1.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-disassemblyMathOnNapkins' original disassembly. Stale since 2018.Tertiary/stale cross-check only.
datacrystal.tcrf.netwebWikiAttempted, 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)webWikiAttempted, 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)

AddressName (zelda3)Cross-check nameSizeCitation
$7E0010main_module_indexMODE (jpdasm, usdasm-lineage, JaredBrian)u8zelda3 src/variables.h:2; JaredBrian Other/symbols_wram.asm:82; jpdasm symbols_wram.asm:81 (three-way exact agreement)
$7E0011submodule_indexSUBMODEu8zelda3 src/variables.h:3; JaredBrian Other/symbols_wram.asm:83; jpdasm symbols_wram.asm:82
$7E00B0subsubmodule_index—u8zelda3 src/variables.h:122
$7E010Csaved_module_for_menu—u8zelda3 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.

ValueFunction nameMeaning
0Module00_IntroTitle screen / attract sequence entry
1Module01_FileSelectFile-select screen
2Module02_CopyFileCopy-file flow
3Module03_KILLFileDelete-file flow
4Module04_NameFileName-entry (new file)
5Module05_LoadFileLoading a save into gameplay state
6Module_PreDungeonPre-dungeon-entry transition/setup
7Module07_DungeonDungeon gameplay
8Module08_OverworldLoadLoad an overworld area, light or dark world
9Module09_OverworldOverworld gameplay, light AND dark world — the world is $7EF3CA, not the module
10Module08_OverworldLoadLoad a SPECIAL overworld area. usdasm names this slot Module0A_OverworldSpecialLoad (bank_00.asm:99); zelda3 reuses the function of 8
11Module09_OverworldSpecial 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
12Module_Unknown0Unused (assert(0) stub) — src/misc.c:203
13Module_Unknown1Unused (assert(0) stub) — src/misc.c:207
14Module0E_InterfaceDialog / menu / HUD-overlay module — see §7 for submodules
15Module0F_SpotlightCloseScreen transition (spotlight closing)
16Module10_SpotlightOpenScreen transition (spotlight opening)
17Module11_DungeonFallingEntranceFalling into a dungeon entrance (pit-warp/entrance animation)
18Module12_GameOverDeath sequence
19Module13_BossVictory_PendantPendant-boss victory sequence
20Module14_AttractAttract-mode demo playback
21Module15_MirrorWarpFromAgaAgahnim mirror-warp cutscene
22Module16_BossVictory_CrystalCrystal-boss victory sequence
23Module17_SaveAndQuitSave-and-quit flow
24Module18_GanonEmergesGanon-emerges cutscene
25Module19_TriforceRoomEnding / triforce room
26Module1A_CreditsCredits
27Module1B_SpawnSelectSpawn-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 — zelda3 src/overworld.c:750
  • Dungeon: Module07_00_PlayerControl — zelda3 src/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.


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".

AddressNameSize/typeMeaningCitation
$7E0020link_y_coordu16Y 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
$7E0022link_x_coordu16X position (pixel)zelda3 variables.h:20; 3-way: JaredBrian :127 (POSX), jpdasm :127
$7E0024link_z_coordu16 (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
$7E00EElink_is_on_lower_levelu8 (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
$7E0476link_is_on_lower_level_mirroru8Mirror/cached copy of the above, read by sprite codezelda3 variables.h:463
$7E002Flink_direction_facingu8Facing 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 guesszelda3 variables.h:29; 3-way: JaredBrian :170 (DIR), jpdasm same offset — UNVERIFIED: exact value↔direction mapping, only the address is triple-confirmed
$7E0066link_last_direction_moved_towardsu8Last direction actually movedzelda3 variables.h:87
$7E0067link_directionu8Current movement direction (distinct from facing)zelda3 variables.h:88
$7E0026link_direction_lastu8Previous facing/direction snapshotzelda3 variables.h:22
$7E0027link_actual_vel_yi8Actual Y velocityzelda3 variables.h:23
$7E0028link_actual_vel_xi8Actual X velocityzelda3 variables.h:24
$7E0030link_y_velu8Y velocity (intended/nominal)zelda3 variables.h:31
$7E0031link_x_velu8X velocity (intended/nominal)zelda3 variables.h:32
$7E005Dlink_player_handler_stateu8, enumLink's top-level state machine — see enum table belowzelda3 variables.h:71; 3-way: JaredBrian :346 (LINKDO), jpdasm same
$7E005Elink_speed_settingu8Speed tier settingzelda3 variables.h:72
$7E0057link_speed_modifieru8Speed modifier (e.g. Pegasus Boots dash multiplier)zelda3 variables.h:63
$7E0056link_is_bunnyu8 (bool)1 = currently a bunny (Moon Pearl missing, in dark world)zelda3 variables.h:62
$7E0055link_cape_modeu8Cape-active statezelda3 variables.h:61
$7E0046link_incapacitated_timeru8Frames Link is incapacitated (knocked back / stunned)zelda3 variables.h:50
$7E031Fcountdown_for_blinku8Post-damage invincibility-with-blink timer, counts down to 0zelda3 variables.h:291; usage confirms it gates damage alongside link_disable_sprite_damage: src/sprite.c:2725, src/sprite_main.c:1498,3846,15769
$7E037Blink_disable_sprite_damageu8 (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 iszelda3 variables.h:351; extremely widely used, e.g. src/player.c:221,364,556,587,1516 (link_disable_sprite_damage++)
$7E0048bitmask_of_dragstateu8, bitfieldDash/drag state bitmaskzelda3 variables.h:52
$7E004Dlink_auxiliary_stateu8Auxiliary state (used with visibility/pose fields)zelda3 variables.h:56
$7E004Blink_visibility_statusu8Visibility status (e.g. hidden during warps)zelda3 variables.h:54
$7E02DAlink_pose_for_itemu8Item-use pose flagzelda3 variables.h:238
$7E0301link_item_in_handu8Currently-wielded/active-use item id (boomerang in flight etc., transient — not the SRAM "equipped" slot)zelda3 variables.h:267
$7E0303current_item_yu8The 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
$7E0202hud_cur_itemu8Cursor index into the inventory grid (used when choosing a new Y item)zelda3 variables.h:189
$7E003Cbutton_b_framesu8Frames 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 (`...
$7E003Dlink_delay_timer_spin_attacku8Spin-attack delay timerzelda3 variables.h:41
$7E0079link_spin_attack_step_counteru8Spin-attack animation step counterzelda3 variables.h:91
$7E031Cstate_for_spin_attacku8Spin-attack sub-statezelda3 variables.h:288
$7E031Dstep_counter_for_spin_attacku8(second) spin-attack step counterzelda3 variables.h:289
$7E0314flag_is_sprite_to_pick_upu8 (bool)Set when standing over a liftable sprite (pot/rock/sign/bush) — "carrying" state gatezelda3 variables.h:282
$7E02F4flag_is_sprite_to_pick_up_cachedu8Cached copy of the abovezelda3 variables.h:260
$7E0359link_sword_typeu80 = 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 sourcezelda3 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
$7E037Alink_position_modeu8Position-lock mode (e.g. during boomerang throw)zelda3 variables.h:350

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.

ValueNameMeaning
0kPlayerState_GroundNormal ground movement
1kPlayerState_FallingIntoHoleFalling into a pit
2kPlayerState_RecoilWallRecoiling off a wall
3kPlayerState_SpinAttackingSpin attack in progress
4kPlayerState_SwimmingSwimming
5kPlayerState_TurtleRockTurtle Rock ice-physics state
6kPlayerState_RecoilOtherRecoiling (other cause)
7kPlayerState_ElectrocutionBeing electrocuted
8kPlayerState_EtherCasting Ether medallion
9kPlayerState_BombosCasting Bombos medallion
10kPlayerState_QuakeCasting Quake medallion
12kPlayerState_FallOfLeftRightLedgeFalling off a side ledge
14kPlayerState_JumpOffLedgeDiagJumping off a diagonal ledge
17kPlayerState_StartDashDash starting (Pegasus Boots)
18kPlayerState_StopDashDash stopping
19kPlayerState_HookshotHookshot travel
20kPlayerState_MirrorUsing the Magic Mirror
21kPlayerState_HoldUpItemHolding up an item (item-get pose) — also the general "lifted object overhead" pose
22kPlayerState_AsleepInBedAsleep in bed (game start)
23kPlayerState_PermaBunnyPermanently a bunny (no Moon Pearl, in a bunny-forcing area)
25kPlayerState_ReceivingEtherReceiving the Ether medallion cutscene
26kPlayerState_ReceivingBombosReceiving the Bombos medallion cutscene
27kPlayerState_OpeningDesertPalaceDesert Palace opening-prayer cutscene
28kPlayerState_TempBunnyTemporarily a bunny
29kPlayerState_PullForRupeesPulling a "grab for rupees" object
30kPlayerState_SpinAttackMotionSpin 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.

AddressNameSizeMeaningCitation
$7EF340link_item_bowu8Bow: 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
$7EF341link_item_boomerangu80=none, 1=Blue, 2=Red (conventional; only nonzero-ness confirmed in source)zelda3 variables.h:1065
$7EF342link_item_hookshotu8 (bool)Hookshot ownedzelda3 variables.h:1066
$7EF343link_item_bombsu8This is the live bomb count, not a boolean (bombs have no separate owned-flag — count itself gates "have bombs")zelda3 variables.h:1067
$7EF344link_item_mushroomu8Mushroom/Powder item statezelda3 variables.h:1068
$7EF345link_item_fire_rodu8 (bool)Fire Rod ownedzelda3 variables.h:1069
$7EF346link_item_ice_rodu8 (bool)Ice Rod ownedzelda3 variables.h:1070
$7EF347link_item_bombos_medallionu8 (bool)Bombos ownedzelda3 variables.h:1071
$7EF348link_item_ether_medallionu8 (bool)Ether ownedzelda3 variables.h:1072
$7EF349link_item_quake_medallionu8 (bool)Quake ownedzelda3 variables.h:1073
$7EF34Alink_item_torchu8 (bool)Fire rod's "lit torch" carry state / lantern-lit flagzelda3 variables.h:1074
$7EF34Blink_item_hammeru8 (bool)Hammer ownedzelda3 variables.h:1075
$7EF34Clink_item_fluteu80=none, 1=shovel(?), 2=flute (conventional tiers; nonzero-ness only is what source branches on)zelda3 variables.h:1076
$7EF34Dlink_item_bug_netu8 (bool)Bug net ownedzelda3 variables.h:1077
$7EF34Elink_item_book_of_mudorau8 (bool)Book of Mudora ownedzelda3 variables.h:1078
$7EF34Flink_item_bottle_indexu8Index/count related to bottle slotszelda3 variables.h:1079
$7EF350link_item_cane_somariau8 (bool)Cane of Somaria ownedzelda3 variables.h:1080
$7EF351link_item_cane_byrnau8 (bool)Cane of Byrna ownedzelda3 variables.h:1081
$7EF352link_item_capeu8 (bool)Magic Cape ownedzelda3 variables.h:1082
$7EF353link_item_mirroru8 (bool)Magic Mirror ownedzelda3 variables.h:1083
$7EF354link_item_glovesu80=none, 1=Power Glove, 2=Titan's Mitt (conventional; only presence directly confirmed)zelda3 variables.h:1084
$7EF355link_item_bootsu8 (bool)Pegasus Boots ownedzelda3 variables.h:1085
$7EF356link_item_flippersu8 (bool)Flippers ownedzelda3 variables.h:1086
$7EF357link_item_moon_pearlu8 (bool)Moon Pearl owned — directly gates bunny transformation, e.g. src/messaging.c:288,320zelda3 variables.h:1087
$7EF359link_sword_typeu8See §2 combat tablezelda3 variables.h:1088
$7EF35Alink_shield_typeu80=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=Mirrorzelda3 variables.h:1089
$7EF35Blink_armoru8Tunic/armor level (0=Green,1=Blue,2=Red conventional)zelda3 variables.h:1090
$7EF35C-$7EF35Flink_bottle_info[0..3]u8×4Per-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 citationzelda3 variables.h:1091
$7EF360link_rupees_goalu16Rupee counter's target value (counts up/down toward link_rupees_actual for the HUD roll animation)zelda3 variables.h:1092
$7EF362link_rupees_actualu16The real spendable rupee countzelda3 variables.h:1093
$7EF364link_compassu16, bitfieldCompass-owned bitfield (bit per dungeon) — layout belowzelda3 variables.h:1094
$7EF366link_bigkeyu16, bitfieldBig-key-owned bitfield, identical bit layout to compasszelda3 variables.h:1095
$7EF368link_dungeon_mapu16, bitfieldDungeon-map-owned bitfield, identical bit layout to compasszelda3 variables.h:1096
$7EF36Alink_rupees_in_pondu8Wishing-pond rupee toss counterzelda3 variables.h:1097
$7EF36Blink_heart_piecesu8Heart-piece count, 0-3 (4th piece rolls into capacity)zelda3 variables.h:1098
$7EF36Clink_health_capacityu8Max HP, in 1/8-heart units (>>3 = heart count) — confirmed unit: src/hud.c:424 kMaxHealthForLevel[link_health_capacity >> 3], src/messaging.c:794zelda3 variables.h:1099
$7EF36Dlink_health_currentu8Current HP, in 1/8-heart unitszelda3 variables.h:1100; 3-way address match not directly grepped but block is contiguous & internally consistent
$7EF36Elink_magic_poweru8Current 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
$7EF36Flink_num_keysu8Current dungeon's small-key countzelda3 variables.h:1102
$7EF370link_bomb_upgradesu8Bomb-capacity upgrade countzelda3 variables.h:1103
$7EF371link_arrow_upgradesu8Arrow-capacity upgrade countzelda3 variables.h:1104
$7EF372link_hearts_filleru8Sub-heart HP fill accumulator (animates HP regen)zelda3 variables.h:1105
$7EF373link_magic_filleru8Sub-unit magic fill accumulatorzelda3 variables.h:1106
$7EF374link_which_pendantsu8, bitfieldPendant 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 settlezelda3 variables.h:1107; src/messaging.c:156,1513-1514; jpdasm symbols_sram.asm:779-781
$7EF375link_bomb_filleru8Sub-unit bomb fill accumulatorzelda3 variables.h:1108
$7EF376link_arrow_filleru8Sub-unit arrow fill accumulatorzelda3 variables.h:1109
$7EF377link_num_arrowsu8Current arrow countzelda3 variables.h:1110
$7EF379link_ability_flagsu8, bitfieldAbility-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 assignmentzelda3 variables.h:1111, src/player.c:2100-2102; jpdasm symbols_sram.asm:795-806
$7EF37Alink_has_crystalsu8, bitfieldCrystal 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 orderzelda3 variables.h:1112; src/messaging.c:157,1517-1518; jpdasm symbols_sram.asm:809-817
$7EF37Blink_magic_consumptionu8Magic-drain-rate modifier (e.g. halved by 1/2 Magic upgrade)zelda3 variables.h:1113
$7EF37C..link_keys_earned_per_dungeon[]u8 arrayPer-dungeon key-earned counters (array, not indexed range confirmed beyond base)zelda3 variables.h:1114
$7EF3C5sram_progress_indicatoru8Progress/"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
$7EF3C6sram_progress_flagsu8, bitfieldResolved 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 documentzelda3 variables.h:1116; jpdasm symbols_sram.asm:853-861
$7EF3C7savegame_map_icons_indicatoru8Controls 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
$7EF3C8which_starting_pointu8Save 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 tablezelda3 variables.h:1118; jpdasm symbols_sram.asm:875-882
$7EF3C9sram_progress_indicator_3u8, bitfieldResolved 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-hobozelda3 variables.h:1119; jpdasm symbols_sram.asm:884-892
$7EF3CAsavegame_is_darkworldu8 (bool)World flag: 0 = Light World, nonzero = Dark Worldzelda3 variables.h:1120
$7EF3CCfollower_indicatoru8Which NPC "tagalong" follower Link currently has (e.g. the value 10 is checked at src/sprite_main.c:1498)zelda3 variables.h:1121
$7EF3CD/$7EF3CFsaved_tagalong_y / saved_tagalong_xu16 eachSaved follower positionzelda3 variables.h:1122-1123
$7EF3D1saved_tagalong_indoorsu8Follower's saved indoors flagzelda3 variables.h:1124
$7EF3D2saved_tagalong_flooru8Follower's saved floor/layerzelda3 variables.h:1125
$7EF3D3follower_droppedu8 (bool)Whether the follower was droppedzelda3 variables.h:1126
$7EF3E7deaths_per_palaceu16 arrayDeath counter per palacezelda3 variables.h:1127
$7EF33Fbyte_7EF33Fu8Unnamed byte immediately before the item block — likely padding/unusedzelda3 variables.h:1063
$7EF300savegame_has_master_sword_flagsu16Master-Sword-related flagszelda3 variables.h:1062
$7EF280save_ow_event_infou8 arrayOverworld 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. $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
  • 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)

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

AddressNameSizeMeaningCitation
$7E001Bplayer_is_indoorsu8 (bool)Indoors flag: 0 = overworld, nonzero = indoors (house/dungeon/cave)zelda3 variables.h:14; 3-way: JaredBrian :115 (INDOORS), jpdasm same
$7E008Aoverworld_screen_indexu16 (low byte is the real 0-127/0x00-0x7F area id; high byte largely unused on this axis)Overworld area indexzelda3 variables.h:97; 3-way: JaredBrian :497 (OWSCR), jpdasm :492-493 (OWSCR/OWSCRH)
$7E00A0dungeon_room_indexu16Dungeon/indoor room idzelda3 variables.h:112; 3-way: JaredBrian :544 (ROOM), jpdasm :539
$7E00A2dungeon_room_index_prevu16Previous room id (for detecting room transitions)zelda3 variables.h:113
$7E048Edungeon_room_index2u16A second/cached room-index copyzelda3 variables.h:475
$7E040Ccur_palace_index_x2u16Dungeon/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)
$7EF3CAsavegame_is_darkworldu8 (bool)World: 0 = Light World, nonzero = Dark World (see §3)zelda3 variables.h:1120
$7E010Ewhich_entranceu8Entrance id used when transitioning between overworld and an indoor areazelda3 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/$7E061Acamera_y_coord_scroll_low/_hiu16 eachCamera Y scroll positionzelda3 variables.h:531-532
$7E061C/$7E061Ecamera_x_coord_scroll_low/_hiu16 eachCamera X scroll positionzelda3 variables.h:533-534
$7E00EElink_is_on_lower_levelu8Layer/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.)

AddressNameCitationMeaning
$7E0B58sprite_stunnedvariables.h:656Auto-decrementing stun timer
$7E0B6Bsprite_flagsvariables.h:660Bitflags incl. tile-hitbox / deflect-arrows / boss-death bits (secondary AsarUSA symbols_wram.asm:3190)
$7E0B89sprite_obj_priovariables.h:666OAM object priority; set in Sprite_TimersAndOam, src/sprite.c:1190,1204
$7E0BA0sprite_ignore_projectilevariables.h:672"Bulletproof" — nonzero means ancillae/projectiles don't interact with this sprite
$7E0BB0sprite_unk2variables.h:673Stores the ancilla ID that last hit this sprite (secondary AsarUSA:3298)
$7E0BC0sprite_N (u8) / sprite_N_word (u16 alias, variables.h:1406)variables.h:674Slot-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)
$7E0BE0sprite_flags5variables.h:675Bitflags incl. tile-interaction/shield-block/damage-SFX/prize-pack (secondary AsarUSA:3376)
$7E0C9Asprite_roomvariables.h:693Overworld screen/room the sprite belongs to (kills sprite on screen transition)
$7E0CAAsprite_defl_bitsvariables.h:694Deflection bits; bit 0x80 = death-flag-ignore/pause-override (src/sprite.c:1494-1498), bit 0x01 = indoor-death-flag skip (src/sprite.c:3633)
$7E0CBAsprite_die_actionvariables.h:695Forced drop on death (secondary AsarUSA:3656: 0=nothing,1=key,2=big key,3=green rupee)
$7E0CD2sprite_bump_damagevariables.h:697Contact damage dealt to Link on touch
$7E0CE2sprite_give_damagevariables.h:698Damage being dealt TO this sprite this frame; subtracted from sprite_health, src/sprite.c:2409-2413
$7E0D00sprite_y_lovariables.h:711Y position, low byte
$7E0D10sprite_x_lovariables.h:712X position, low byte
$7E0D20sprite_y_hivariables.h:713Y position, high byte
$7E0D30sprite_x_hivariables.h:714X position, high byte; combined via Sprite_Get16BitCoords, src/sprite.c:1207-1210
$7E0D40sprite_y_velvariables.h:715Y velocity
$7E0D50sprite_x_velvariables.h:716X velocity
$7E0D60sprite_y_subpixelvariables.h:717Y sub-pixel accumulator
$7E0D70sprite_x_subpixelvariables.h:718X sub-pixel accumulator
$7E0D80sprite_ai_statevariables.h:719Per-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..0DE0sprite_A,sprite_B,sprite_C,sprite_Dvariables.h:720-725Generic per-type scratch bytes (bespoke meaning per handler; sprite_C is often reused as an HP-like counter by specific handlers)
$7E0DC0sprite_graphicsvariables.h:723Graphics/animation-frame control
$7E0DD0sprite_statevariables.h:724Top-level lifecycle dispatch index — see 5.2/5.3
$7E0DF0/0E00/0E10sprite_delay_main/_aux1/_aux2variables.h:726-728Auto-decrementing timers, src/sprite.c:1171-1176
$7E0E20sprite_typevariables.h:729Sprite type ID (0x00-0xF2) — dispatch index into kSpriteActiveRoutines[243], see 5.4
$7E0E30sprite_subtypevariables.h:730Per-type auxiliary subtype byte
$7E0E40sprite_flags2variables.h:731Bitflags incl. OAM-slot count (&0x1f)+1)*4, src/sprite.c:1159); secondary bits: harmless/master-sword-related/wall-related/OAM-count
$7E0E50sprite_healthvariables.h:732Generic hit-point counter — see 5.3
$7E0E60sprite_flags3variables.h:733Bitflags: death-anim/impervious/shadow-size/has-shadow/OAM-palette/OAM-nametable
$7E0E70sprite_wallcollvariables.h:734Wall-collision-related flags
$7E0EB0sprite_head_dirvariables.h:738Facing/movement direction
$7E0EC0sprite_anim_clockvariables.h:739Animation clock
$7E0EF0sprite_hit_timervariables.h:742Hit/knockback-recoil timer; high bit = "recoiling" flag, src/sprite.c:1180-1204
$7E0F00sprite_pausevariables.h:743Per-sprite pause flag, src/sprite.c:1493-1498
$7E0F20sprite_floorvariables.h:745Floor/layer (0/1, BG1 vs BG2)
$7E0F30/0F40sprite_y_recoil/sprite_x_recoilvariables.h:746-747Knockback/recoil offsets
$7E0F50sprite_oam_flagsvariables.h:748OAM attribute flags actually copied to hardware OAM
$7E0F60sprite_flags4variables.h:749Bitflags: ignore-collision / stasis (doesn't count toward room-clear) / activeness / hitbox ID
$7E0F70/0F80/0F90sprite_z/sprite_z_vel/sprite_z_subposvariables.h:750-752Height/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).

IDNameNotes
$00Raven
$01Vulture
$02StalfosHead
$08, $0AOctorok (2 color variants, shared handler)
$09GiantMoldorm
$0BCucco
$0COctorokStone
$0DBuzzblob
$11Hinox
$12Moblin
$41-$43BlueGuard (×3)soldier variant family
$44BluesainBoltsoldier variant ("UsainBolt" in jpdasm — cosmetic naming difference only)
$45HogSpearMansoldier variant
$46BlueArchersoldier variant
$47GreenBushGuardsoldier variant
$48RedJavelinGuardsoldier variant
$49RedBushGuardsoldier variant
$4ABombGuardsoldier variant
$4BGreenKnifeGuardsoldier variant
$4CGeldman
$4DToppo
$6DRat
$6ERope
$6FKeese (bat)
$73Uncle and Priest
$76Zelda
$78Mrs. Sahasrahla
$7AAgahnim
$B4Purple Chestthe only sprite-table chest (roaming quest item) — see note below on ordinary chests
$C7Pokey
$D8Heart drop
$D9-$DBRupee drop (Green/Blue/Red, per jpdasm cross-check)
$E4-$E5Key drop (Small Key / Big Key, per jpdasm cross-check)
$EAHeart Container
$EBHeart 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:

AddressNameCitationMeaning
$7E0BFAancilla_y_lovariables.h:677Y position low byte
$7E0C04ancilla_x_lovariables.h:678X position low byte
$7E0C0Eancilla_y_hivariables.h:679Y position high byte
$7E0C18ancilla_x_hivariables.h:680X position high byte
$7E0C22ancilla_y_velvariables.h:681Y velocity
$7E0C2Cancilla_x_velvariables.h:682X velocity
$7E0C4Aancilla_typevariables.h:685Type ID, 1-based (0=empty slot) — dispatch src/ancilla.c:661-677
$7E0C54ancilla_stepvariables.h:686Per-type step/sub-state scratch
$7E0C5Eancilla_item_to_linkvariables.h:687Tracks hookshot extension / item-receipt ID (secondary AsarUSA:3534)
$7E0C68ancilla_timervariables.h:688General auto-decrementing timer, src/ancilla.c:674-675
$7E0C72ancilla_dirvariables.h:689Direction
$7E0C7Cancilla_floorvariables.h:690Floor/layer (0/1), src/ancilla.c:764-766
$7E0C90ancilla_numsprvariables.h:692Number 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)

IDMeaningIDMeaning
1Somaria bullet2Fire Rod shot
4Sword-beam impact5Boomerang
6Generic wall-hit7Bomb
8Door debris9Arrow
10Arrow stuck in wall11Ice Rod shot
12Sword beam (full-health charge)13Full-charge spin spark
17Ice Rod wall-hit19Ice Rod sparkle
21Jump/water splash22Hit-stars stun effect
24Ether medallion25Bombos medallion
26Magic Powder dust28Quake medallion
31Hookshot34Item-get animation
40Wishing-pond item toss49Cane of Byrna shield spark
51Blast-wall explosion53Master Sword receipt
54Flute effect58Big Bomb explosion
63Bush destruction poof67Ganon'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 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.
  • 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 in PrepareDungeonExitFromBossFight (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
0x00Standard floor (walkable)
0x01-0x04, 0x26, 0x43Solid / wall collision
0x08Deep water
0x09Shallow water
0x0DSpike floor
0x0E/0x0FIce (GT / Ice Palace)
0x10-0x13, 0x18-0x1BDiagonal slopes (4 orientations ×2 variants)
0x1D-0x1F, 0x3D-0x3FAuto-stairs / layer-swap (N/S)
0x20, 0xB0-0xBDPit / Somaria-platform-pit variants
0x22, 0x30-0x39Manual and straight inter-room stairs
0x28-0x2FLedges (N/S/E/W + diagonal)
0x44Spike
0x46Desert Palace tablet trigger
0x50-0x56Liftable objects (bush/rock/sign variants)
0x58-0x5DChest 0-5 (value directly encodes which chest slot, matching kChestOpenMasks index, §6.1)
0x5E-0x5FSpiral stairs
0x60Rupee tile
0x63Minigame chest
0x67-0x6BCrystal peg / conveyors
0x80-0xAFDoor/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-0xCFLightable torches
0xD0-0xFFMostly 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:

TableSNES addressSizezelda3's own extractor
kMap16ToMap8$8F80003752 × 4 wordsassets/compile_resources.py:155
kMap8DataToTileAttr$8E9459512 bytesassets/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

AddressNameSizeMeaningCitation
$7E1CD4text_render_stateu8Index 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
$7E1CD8messaging_moduleu8Sub-mode within the messaging systemzelda3 variables.h:820
$7E1CE0text_wait_countdownu16Generic text timing countdownzelda3 variables.h:825
$7E1CE8choice_in_multiselect_boxu8Which option is highlighted in a 2/3-choice text promptzelda3 variables.h:828
$7E1CE9text_wait_countdown2u8Post-keypress debounce countdown (28 frames) used by both the "wait for key" and "end message" text commandszelda3 variables.h:829; usage src/messaging.c:2469-2489
$7E1CF0dialogue_message_indexu16Which dialogue message is loaded/showingzelda3 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_indexFunctionMeaning
0Module_Messaging_0Unused stub (assert(0))
1Hud_Module_RunHUD update pass
2RenderTextText box actively rendering (see rule above)
3Module0E_03_DungeonMapDungeon map screen (opened via the X/map button in a dungeon, gated src/dungeon.c:6602-6608 on having a valid palace+room)
4Module0E_04_RedPotionDrinking a red potion (HP refill animation)
5Module0E_05_DesertPrayerDesert Palace opening-prayer cutscene
6Module_Messaging_6Unused stub
7Messaging_OverworldMapWorld map screen (opened via Select/map button on the overworld, src/overworld.c:760-763)
8Module0E_08_GreenPotionDrinking a green potion (magic refill)
9Module0E_09_BluePotionDrinking a blue potion (both refill)
10Module0E_0A_FluteMenuFlute-warp menu
11Module0E_0B_SaveMenuSave/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)

PropertyValueSource
Unheadered file size1,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 size1,049,088 bytes (0x100200) — unheadered size + 512-byte copier headerzelda3 assets/util.py:63-65 (same check)
Header strip methodIf len(ROM) % 0x100000 == 0x200, drop the first 0x200 (512) byteszelda3 assets/util.py:63-65, verbatim: if (len(self.ROM) & 0xfffff) == 0x200: self.ROM = self.ROM[0x200:]
USA v1.0 SHA-1 (unheadered)6D4F10A8B10E10DBE624CB23CF03B88BB8252973zelda3 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)608C22B8FF930C62DC2DE54BCD6EBA72spannerisms/usdasm README.md (Binaries section); matches the No-Intro-attributed web search result
USA v1.0 CRC32 (unheadered)777AAC2Fspannerisms/usdasm README.md; matches the No-Intro-attributed web search result
USA v1.0 CRC32 (headered, per web search only)DD42510EWeb 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 / 50F2spannerisms/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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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).

TableSNESSizeExtractor
kOverworld_Entrance_Area$9BB96F129 wordszelda3 assets/extract_resources.py:128; usdasm bank_1B.asm:392
kOverworld_Entrance_Pos$9BBA71129 words:129; usdasm :525
kOverworld_Entrance_Id$9BBB73129 bytes:130; usdasm :658
kFallHole_Pos$9BB80019 words:138; usdasm bank_1B.asm:215
kFallHole_Area$9BB82619 words:139; usdasm :236
kFallHole_Entrances$9BB84C19 bytes:140; usdasm :257
EntranceData.room_id$02C813134 wordsusdasm 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:

ByteWhatNeeds
$50green bushnothing
$51dark bushnothing
$52small grey rockPower Glove
$53small black rockTitan's Mitt
$54signnothing
$55big grey rockPower Glove
$56big black rockTitan'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).