jevsnes.git / research / castle-inside.md
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.