Inside Hyrule Castle: from uncle to Zelda's cell
Written 2026-09-21 from zelda3 (src/dungeon.c, src/player.c,
src/sprite_main.c, assets/extract_resources.py), the Asar USA disassembly
(AsarUSALTTPDisassembly/Bank04.asm) and usdasm, with print_room /
get_exit_datas from zelda3's extractor run read-only against the USA ROM.
Positions are pixels inside the room's 512x512 supertile unless marked world.
It exists because Goal::RescueZelda had no route from the moment Link took
the sword, and the reason was not where anyone had been looking.
Why RescueZelda had no route
Room $55 (uncle, the secret passage) has no door or stairs into the
castle. Its only exits are the hole Link fell in by and a door of type $12
(exit to the overworld) in its south wall at (112, 464). The overworld exit
table puts Link back outside at world (2248, 1736) in area $1B, inside the
castle walls. So the goal's target, the front door (entrance $04, world
(2040, 1784)), is right: what was missing was getting out of $55 first.
plan::heading only answers for an outdoor target from outdoors, so indoors
it said nothing. Fixed by plan::way_out (jev 8d087157). This probably also
explains the "inner grounds unreachable" row in castle-passage.md: the inner
courtyard is entered from this exit, not from the moat side. Inferred, not yet
measured with walkcheck --spots.
Freeing Zelda does not set $7EF3C5 = 2. Her cell script sets
follower_indicator ($7EF3CC) to 1 (sprite_main.c:6300-6305); progress 2
is set only at the Sanctuary (:6342). Goal::RescueZelda now reads the
follower (alttp::Follower).
The route
| Step | How |
|---|---|
$55 (dark) → overworld | South exit door (112, 464); walk from world (2248, 1736) to entrance $04 |
$61 (1F) | Arrive at the south door, about (240, 440) |
$61 → $60 | West door (16, 120) or (40, 248) |
$60 → $50 | North door (368, 56) |
$50 → $01 | East warp door (440, 120): header slot stairs3 = $01, not $51 |
$01 → $72 (B1) | Spiral stairs down at (240, 80) |
$72 → $82 | Opening in the bottom edge at (48, 448), room +16 |
$82 → $81 | West-edge openings at (0, 112) or (0, 384), room -1 |
$81 → $71 | North door (112, 56), room -16 |
$71 → $70 (B2) | Spiral stairs down at (152, 56) |
$70 → $80 (B3) | Spiral stairs down at (72, 32), stair slot 1 |
The east wing mirrors it: $61 → $62 → $52, then $52's west warp door →
$01. Floors are counted from $61 as 1F, not read from a floor table.
How each transition works (src/dungeon.c)
- Edge. Room -1 at
:2074, +1 at:7987, -16 at:2123, +16 at:2157, only when Link crosses the ROOM boundary (a quadrant boundary just scrolls). A$89floor tile is a warp door: it goes to header slot stairs2 going left, stairs3 going right (:2067,:7980). An$8Etile on an up/down edge exits to the overworld (:2106,:2150). - Stairs.
Dungeon_DetectStaircase(:4302-4357):$26/$38/$39/$5E/$5Fat Link's feet with a$30-$37tile one row below; the destination is header slotstairs[attr & 3](:4338-4339). The engine numbers staircases$30+ running index, up stairs first then down from$34(Dungeon_LoadObjectAttribute,:3865-3930). Stairs on layer 2 go in the second half of the collision grid (:1524), read only when$EE!= 0 (:4310) - which applies to$01and$71; the value of$EEthere is unverified. - Holes. Header byte 9 (
player.c:1545). None on this route.
Reading it from the ROM
- Room header pointers at SNES
$04F502(PC$027502), a word per room into bank$04(extract_resources.py:380; loaderdungeon.c:3670-3719). Byte 0 bg2/collision/dark (bit 0); 1-6 palette, blockset, spriteset, effect, tag1, tag2; 7 hole and stairs0-2 planes; 8 stairs3 plane; 9 hole destination; 10-13 stairs0-3 destinations. Headers can stop short at 7 or 11 bytes - the rest is the next room's header - so a slot is read only when the room has a staircase or hole that uses it. - Door lists at
$1F83C0(usdasmrooms.asm:329), two bytes a door: position 0-11 in the high nibble of byte 0, direction N/S/W/E = 0-3 in its low two bits, type in byte 1 (dungeon.h:5-38;dungeon.c:83-86maps position to tile). Types on this route:$1Csmall-key door,$44shutter,$46warp door,$12exit to the overworld,$16layer change. - Chests
$01E96E; room sprites (including key carriers)$09D62E; overworld exits$02DD8Aonward (extract_resources.py:32-45).
| Room | Header bytes | Bank04.asm |
|---|---|---|
$55 | 01 01 10 0D 00 00 00 | :7811 |
$61 | C0 00 00 04 00 00 00 08 00 00 51 | :8076 |
$60 = $62 | C0 00 00 04 00 00 00 | :8054 |
$50 = $52 | C0 00 00 04 00 00 00 00 00 00 00 00 01 01 | :7701, :7745 |
$01 | … 72 00 50 52 | :6193 |
$72 | C0 01 01 04 00 00 00 08 00 00 01 | :8386 |
$81 = $82 | C0 01 01 04 00 00 00 | :8653 |
$71 | C0 01 01 04 00 08 00 00 00 00 70 | :8364 |
$70 | 00 01 01 04 00 00 00 08 00 00 71 80 | :8342 |
$80 | 60 01 01 04 00 00 00 00 00 00 70 | :8631 |
What gates the route
$72: small-key door at (240, 288) between the landing and the way on; the key is carried by the soldier at (272, 96) in the same upper part.$71: a kill-the-enemies tag on the south half with shutters at (112, 440) and (296, 376); the blue soldier at (416, 384) carries the key for the small-key door at (112, 288) in front of the stairs down.$80: the ball-and-chain trooper at (416, 144) drops the big key; the cell lock at (352, 112) opens like a big-key chest (dungeon.c:1696-1704,:4041-4047). Zelda is at (352, 48). The chest at (328, 64) holds the lamp (whether it is inside the cell is unverified).- Dark rooms on the route: only
$55.
What the harness still lacks for the indoor half
nav::dungeon has the graph and the search (edge / stairs / drop links,
fewest-transition BFS). Not yet built: a room-header and door-list reader in
alttp (same read_bytes/read_words helpers entrance.rs uses) that
produces those links from the ROM; a Target::Room for goals inside a
dungeon (RescueZelda's real target is room $80 once Link is through the
front door); mapping a staircase exit in the scene to its header slot
(attr & 3 of the $30-$37 tile under it); warp doors ($89) as their own
exit sort, since today they read as a generic Door; and fighting the key
carriers, which the fight candidate (Then::Strike) makes possible.