1# Hyrule Castle's secret passage, and why every run stopped short of it 2 3Written 2026-09-20 from primary sources (zelda3 `src/`, `assets/`; usdasm 4`bank_*.asm`) and from measurements taken with `apps/walkcheck --map` against 5the real USA ROM. It exists because a whole session was spent believing the 6harness could not cross Hyrule Castle's moat, and the moat was innocent. 7 8## The one fact that mattered 9 10**`kFallHole_Pos` is not the position of the hole.** It is 128 pixels - eight 11map16 rows - above it. 12 13`Overworld_GetPitDestination` (zelda3 `src/overworld.c:3194-3215`) builds its 14lookup key from `link_x_coord`/`link_y_coord`, but it is called from 15`Link_HandleFall` **after** that function has already moved `link_y_coord` up 16by the camera offset: 17 18```c 19// src/player.c:1531,1547,1552 20uint8 y = (uint8)(link_y_coord - BG2VOFS_copy2); 21... 22link_y_coord = link_y_coord - y - 0x10; 23Overworld_GetPitDestination(); 24``` 25 26So the table is built against a key that is not where the hole is, and 27zelda3's own tooling says so in one line each way: 28 29| Direction | Line | 30| --- | --- | 31| Reading the ROM | `assets/extract_resources.py:137` — `pos = get_word(0x9BB800 + i * 2) + 0x400` | 32| Writing it back | `assets/compile_resources.py:323` — `x << 1 \| ((y - 8) & 0x3f) << 7` | 33 34`$0400` is eight rows of 128 pos units. `packages/alttp/src/entrance.rs` 35applies it to holes and **not** to doorways, which carry no such fudge. 36 37### What it cost 38 39Entrance `$7D` decoded to world (2432, 1568): a `Wall` tile in the deep water 40at the top of area `$1B`, with no land near it. Every run therefore reported 41"the goal is unreachable", from anywhere, forever — and because the scene says 42`cannot get there` whether the map does not join up or the target is in the 43sea, it read as a collision defect for a whole session. Corrected, the hole is 44at **(2432, 1696)**, and the flood reaches it in 148 steps from south of the 45moat. 46 47### Cross-check, from a table with nothing to do with the first 48 49`OverworldData_HiddenItems_Screen_1B` (usdasm `bank_1B.asm:2359`): 50 51``` 52|1BC4E5| dw $0570 : db $80 ; Hole xy:{ 0x380, 0x0A0 } 53``` 54 55`$380` and `$0A0` are tiles 56 and 10, which is (2432, 1696). Two tables, one 56answer. 57 58## The hole is under a bush, and the bush grows back 59 60Type `$80` is a **secret**: `Overworld_RevealSecret` (`src/overworld.c:3556-3595`) 61returns `kTileBelow[0]` = map16 `$0DCC`, whose four quadrants are map8 `$024` 62and `$035`, both behaviour **`$20`, a pit** (usdasm `bank_0F.asm:3538`, 63`bank_0E.asm`). Before it is revealed the tile is map16 `$0036`, behaviour 64`$50` — a green bush, solid and liftable. 65 66Lifting is what reveals it. `Overworld_LiftingSmallObj` (`:3393-3411`) calls 67`Overworld_RevealSecret(pos)` and, when it returns non-zero, writes that map16 68**straight into the `$7E2000` buffer** (`:3405`) — the same buffer 69`alttp::Outside` reads every frame. So the hole appearing is something the 70harness simply sees; nothing has to be predicted. 71 72**It is not persistent.** `Overworld_Memorize_Map16_Change` (`:2727-2735`) 73fills a RAM list that every area load zeroes (`:934, 1782, 1868, 1931, 1980, 742024`), and no `save_ow_event_info` bit is set for a `$80` secret. The bush is 75back every time Link re-enters the area, so "walk there, lift, step in" is a 76plan and not a one-off. That is `brain::Then::LiftAndEnter`. 77 78## The map of area `$1B`, measured 79 80`walkcheck --state castle --map`, 2026-09-20. Area `$1B` is 1024x1024 at 81(1536, 1536); the four screens `$1B`/`$1C`/`$23`/`$24` share it. 82 83- **No unnamed behaviour bytes at all.** The area uses 31 distinct values and 84 every one has a name. Tile classification was never the blocker here. 85- `$27` (1,065 cells) is the castle's iron fence — `TileBehavior_Hookshottables`, 86 `R14 |= bits` (`src/tile_detect.c:342-344`), genuinely solid. 87- The **moat** is `$08` deep water (1,943 cells) and the **bridge** is ordinary 88 `$00` ground between `$01`/`$02` railings, at x 1992-2088, running from 89 y 2424 up into the courtyard. There is no "bridge" tile behaviour in the 90 engine and no mention of a castle moat anywhere in zelda3 or the three 91 disassemblies; bridges in this game are drawn tiles over plain ground. 92- The flood from south of the moat crosses the bridge, spreads through the 93 courtyard band at y 2208-2264, and walks both the west (x 1624-1712) and 94 east (x 2384-2496) perimeter corridors all the way north. 95 96### What is still unreachable, and is a real gap 97 98The castle's **inner** grounds are not reached by the flood, so the four 99doorways filed under the area are all `no route`: 100 101| Entrance | Room | At | Reached | 102| --- | --- | --- | --- | 103| `$03` | 96 | (1832, 1576) | no | 104| `$04` — the front door | 97 | (2040, 1784) | no | 105| `$05` | 98 | (2248, 1576) | no | 106| `$24` | 224 | (2040, 1624) | no | 107| `$32` — walk-in secret entrance | 85 | (2248, 1752) | no | 108| `$7D` — the hole | 85 | (2432, 1696) | **148 steps** | 109 110The what-if pass (`nav::Ground::treating_as_open`, one class at a time) says 111all five open at once if `Wall` is opened and none of them open for any other 112class. So the barrier is something we read as `$01`/`$02`/`$26`/`$43`/`$42` 113and the engine does not, OR the inner grounds are genuinely gated in a way 114this has not found — a gate sprite, a `$70`-`$7F` manipulable tile, or a 115one-way ledge taken in a direction `alttp::Hop` does not model. **It has not 116been measured against the engine**, and the way to do that is to put Link on 117the frontier and run `walkcheck --spots` there. 118 119It matters for `Goal::RescueZelda`, whose target is entrance `$04`. It does 120not matter for `Goal::FindUncle`, which is the hole. 121 122## Ledge directions, not yet acted on 123 124`alttp::Hop::ways` is narrower than the engine. From `src/player.c`: 125 126| Byte | Vertical | Horizontal | 127| --- | --- | --- | 128| `$28` north | hop, either way, landing searched northward (`:4479-4487, 4675-4706`) | **blocked** (`:5079`) | 129| `$29` south | hop (`:4465-4476`) | blocked | 130| `$2A`/`$2B` | blocked (`:4459`) | hop, left **or** right by walking direction (`:5098-5122`) | 131| `$2C`/`$2E` | hop (`:4508-4517`) | **also** hop (`:5141-5151`) | 132| `$2D`/`$2F` | hop, with a sideways component (`:4490-4505`) | also hop (`:5154-5168`) | 133 134We model the diagonals as vertical-only. Widening them would open routes; it 135would also open wrong ones, so it wants a `walkcheck --spots` measurement 136before and after rather than a patch. 137 138Every ledge also goes through `RunLedgeHopTimer` (`:4572-4588`), which 139restores Link's coordinates for about 19 frames — so for a third of a second a 140ledge behaves exactly like a wall. Anything that decides "Link did not move, 141therefore that direction is blocked" on a shorter window than that will 142blacklist every ledge in the game. 143 144## Bytes that can occur outdoors at all 145 146The overworld's behaviour byte comes only from `kMap8DataToTileAttr`, a 147512-byte ROM table at `$8E9459` (usdasm `bank_0E.asm:205-268`, 148`OverworldTileTypes`). Its whole distinct value set is 149 150``` 15100 01 02 08 09 10 12 18 1A 20 22 27 28 29 2A 2B 2C 2D 2E 2F 15240 42 43 44 46 48 4B 4E 4F 50 51 52 53 54 55 56 57 153``` 154 155plus `11 13 19 1B` from the horizontal-flip OR (`src/tile_detect.c:25-27`). 156Everything else is indoors-only in vanilla. `alttp::Tile::outdoors` still 157names all 256, because a classifier with a hole in it reads an unknown wall as 158open.