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