1# zbanks/alttp on our console 2 3[zbanks/alttp](https://github.com/zbanks/alttp) is a bot, written in C, that 4plays *A Link to the Past* from SNES memory. Upstream it is linked into a 5patched Snes9x; here it is compiled from upstream's tree 6(`third-party/c/zbanks-alttp`, commit bcb2537, "Hammer/sword/lift obstacles 7in path in ap_follow_targets") plus the carried patches in 8`third-party/c/patches/` 9and linked into our own host, `packages/zbanks`, which drives 10`packages/console`. This page is the contract between the two, every way our 11host differs from upstream's, and the evidence for each difference. 12 13User ruling, 2026-09-21 12:34: *"what you have build does NOT work. the repo i 14shared DOES work. i want the latter ported to rust, faithfully."* Narrowed at 1512:36: *"it doesn't need ported to rust if you can wrap and run the C."* So 16the bot's own code is not edited by the host; everything below is host, 17except the carried patches in `third-party/c/patches/`: two regressions in 18upstream's own last commits ("The stairs", "The Armos Knights"), the goal 19choice hook, and the `$B8` room - each a change no host can make. 20 21Citations are `file:line` in `third-party/c/zbanks-alttp/` unless another 22repository is named. Reference clones: `~/src/github.com/zbanks/alttp`, 23`spannerisms/usdasm` and `spannerisms/jpdasm` (the USA and Japanese 24disassemblies), `sporchia/alttp_vt_randomizer` (219fcaf) and 25`KatDevsGames/z3randomizer` (the Randomizer's patcher and its ASM). 26 27## The host contract 28 29Upstream's host is `snes9x.patch` (unix/unix.cpp), and it gives the bot 30exactly this: 31 32| Upstream | What it is | Ours (`packages/zbanks/src/lib.rs`) | 33| --- | --- | --- | 34| `struct ap_snes9x` (`alttp_public.h:10-15`) | Four fields: `base`, `save`, `load`, `info_string_ptr`. | `ApSnes9x`, `#[repr(C)]`, same order. | 35| `base(addr)` = `S9xGetMemPointer(addr)` | A pointer to the byte at a SNES bus address. `ap_init` calls it once per RAM variable and caches the pointer forever (`ap_snes.c:60-61`); `ap_map_attr_from_ram` calls it per lookup (`ap_map.c:364-365`). The bot READS and WRITES through it. | Pointers into a WRAM mirror and a ROM copy that never move (leaked allocations). WRAM is copied in from the console before every tick and back after it, which is how the bot's writes reach the game. | 36| `save(name)` / `load(name)` = `S9xFreezeGame` / `S9xUnfreezeGame` | Named savestates in the working directory; nonzero on success. `ap_init` calls `load("home")` then `load("hpegs")` (`ap_snes.c:66-81`) and ignores the result. | Console snapshots, `<name>.state` in a states directory the host chooses. | 37| `info_string_ptr` = `&GFX.InfoString` | Snes9x draws this string over the picture. The bot points it at `ap_info_string` every tick (`alttp.c:15`). | A slot the panels read. | 38| `ap_tick(IPPU.TotalEmulatedFrames, &jpb)` then `S9xMainLoop()` | Once per frame, BEFORE the frame runs, with the frame count and the human's pad (`MovieGetJoypad(0)`); the pad it leaves is the pad for that frame, and the human's is put back afterwards. | `Bot::tick(console, states, human)` returns the pad; the host holds it and runs one frame. Frame numbers start at 0 and run on across loads, as `TotalEmulatedFrames` does. | 39| joypad word | Snes9x layout: bit 15 B, 14 Y, 13 Select, 12 Start, 11 Up, 10 Down, 9 Left, 8 Right, 7 A, 6 X, 5 L, 4 R (`ap_snes.h:6-17`). | `PAD_BITS`. Opposing directions are dropped by both emulators (Snes9x `UpAndDown` off by default; jgenesis `allow_opposing_joypad_directions: false`, snes-core api.rs:63). | 40 41What `base()` is asked for (every `X(...)` in `AP_RAM_LIST`, 42`ap_snes.h:40-176`, plus the three literal lookups in `ap_map.c`): 43 44- work RAM, `$7E0000-$7FFFFF`, and its mirror in bank `$00` below `$2000` 45 (`$000438`, `$00047E`, ... `$0006B0`); 46- ROM tables in LoROM banks `$01`, `$02`, `$06`, `$0F`, `$1B`; 47- nothing else - no cartridge SRAM, no VRAM, no I/O. A request for anything 48 else is counted (`Report::stray_reads`) and has always been 0. 49 50`assert_bp(x)` (`ap_macro.h:79`) executes `int3` when `x` is false - a 51breakpoint for the gdb upstream ran under. Outside a debugger that is SIGTRAP, 52which kills the process. `shim/host.c` installs a SIGTRAP handler that counts 53the trap, keeps its address and returns: exactly gdb's `continue`. Plain 54`assert` is left alone and still aborts. 55 56The bot writes debug files (`goals.txt`, `map`, `map.pbm`, `full_map.dot`, 57`screens.txt`, ...) with bare relative names (`ap_map.c:546-547, 653, 1639, 581645, 1732, 1761, 1769, 1877, 1884, 1899`, `ap_plan.c:37`). The library is 59compiled with `-Dfopen=zb_fopen -Drename=zb_rename`, which sends them into 60the directory the host names (`run/zbanks-c/<run>/` headless, 61`run/zbanks-window/` in the app) instead of the repo root. 62 63Build flags are upstream's Makefile's, with three changes a newer compiler 64forces (`third-party/c/BUCK`): `-fcommon` (`ap_snes.h:32` defines 65`bool ap_manual_mode;` in a header; gcc < 10 merged it, clang 21 fails the 66link), no `-Werror`, and the two defines above. 67 68## Vanilla is not the Randomizer 69 70Upstream played the Randomizer, "Open mode, Sword on Uncle, No Glitches, 71Defeat Ganon", cheating "infinite health, bombs, and arrows" (README). Our 72cartridge is USA 1.0, SHA-1 `6d4f10a8…`. Each difference that reaches the 73bot, and what the host does about it: 74 75### ROM layout: the Randomizer is built on the JAPANESE 1.0 ROM 76 77The Randomizer requires "Zelda no Densetsu: Kamigami no Triforce (Japan) 78v1.0", CRC32 0x777AAC3F (alttpr.com/en/start; pyz3r). Work RAM is laid out 79the same in both, but ROM code and data moved, and the bot hardcodes 80Japanese ROM addresses. The first run on our ROM died at once on 81`assert(n_chests == 0 || chest_index != 168)` (`ap_map.c:3201`): it searched 82the chest table at `$01E96C`, which in the USA ROM starts two bytes later. 83 84Every ROM table the bot reads was located in the USA ROM by taking its bytes 85from the Japanese disassembly (`jpdasm`) and searching our image, then 86checked byte-for-byte over the whole table: 87 88| Japanese | USA | Table | Bot's name | 89| --- | --- | --- | --- | 90| `$01E96C` | `$01E96E` | `RoomData_ChestItems`, 168 × 3 | `dungeon_chests` | 91| `$02CCBD` | `$02CF59` | `EntranceData` Y | `entrance_ys` | 92| `$02CDC7` | `$02D063` | `EntranceData` X | `entrance_xs` | 93| `$02EB29` | `$02EDC5` | `.bombable_door_location` | `over_overlay_map16s` | 94| `$06F735`..`$06F7D5` | 6 bytes lower | sprite hitbox tables, 6 × 0x20 | `hitbox_*` | 95| `$0FFD94` | `$0E9459` | `OverworldTileTypes`, 0x200 | `over_tattr2`, and `LOAD2(0xFFD94 + x)` (`ap_map.c:417`; its own comment cites the USA `DATA_0E9459`) | 96| `$0F8000` | same | `Map16Definitions` | `LOAD2(0xF8000 + x)` - the USA table has six more entries, but work RAM holds USA map16 numbers, so the USA table is the right one | 97| `$1BB800`..`$1BBB73`, `$1BF110` | same | overworld entrance / hole tables, tile types | `over_hle_*`, `over_ent_*`, `over_tattr` | 98 99`base()` answers each Japanese address from the USA table (`JP_TO_US`). 100 101### Open mode: a save past the rain, and uncle who does not bring it back 102 103Open mode is in the ROM and the save, not in the bot. The Randomizer's 104`Rom::setOpenMode` (alttp_vt_randomizer app/Rom.php:1554-1563) writes the 105initial save: game state `$7EF3C5 = 2` (Zelda rescued, so no rain), progress 106flags `$7EF3C6 |= 0x14`, start at Link's house `$7EF3C8 = 1`, castle gate 107open (overworld `$1B` |= 0x20); every Randomizer file also starts with the 108Kakariko bomb hut and brewery open and ability flags `0x68` 109(InitialSram.php:24-31). `zbanks::rando::preset_file` puts those bytes into 110save file 1 and redoes its checksum (sum of words = 0x5A5A, usdasm 111bank_00.asm:1698-1718). 112 113In vanilla, uncle spawns in the secret passage while `$7EF3C6` bit 0 is 114clear, whatever the game state (`SpritePrep_Uncle`, usdasm 115bank_05.asm:16216-16240), so "Sword on Uncle" works on the preset save as it 116does in the Randomizer - but vanilla `Uncle_GrantEquipment` then stores 1 to 117the game state (`$05DF65`, bank_05.asm:17083-17084), which brings the rain 118back. The Randomizer NOPs those six bytes (z3randomizer hooks.asm:1560-1562, 119"Open Mode Fixes"); `zbanks::rando::patch_rom` does the same to our image (the first row of `PATCHES`). 120Vanilla uncle gives the fighter sword AND shield (ITEMGET 00); the 121Randomizer's gives one item. 122 123Uncle also speaks first. `Uncle_LyingInDefeat` calls `ShowMessageOnContact` 124with message `$0E` and only moves on to `Uncle_GrantEquipment` when it 125returns carry (usdasm bank_05.asm:17042-17059). The Randomizer blanks that 126message: `uncle_dying_sewer` is in `Text::removeUnwanted`'s list and becomes 127`{NOTEXT}` (alttp_vt_randomizer app/Text.php:1096, 1107). The bot relies on 128it: its `TALK_NPC` task ends the moment uncle is touched ("Return success 129early from the uncle", ap_plan.c:466-469), and the next task clears A, so a 130vanilla text box holds it for good - measured: frame 13,000 onward, module 131`$0E`, "Unnh... G, I didn't want you involved in this...". `rando::PATCHES` 132replaces that `JSL ShowMessageOnContact` (`$05DF34`) with the contact test 133the routine itself starts with, `JSL CheckDamageToLink_same_layer_long` 134(`$06F129`, bank_05.asm:17556): the same carry on touching him, no box. 135 136Measured before this was done: from a vanilla rain-state start the bot 137walked out of the house and stood in the first hint soldier's text box for 138the rest of the run. 139 140### No item-get text 141 142In vanilla, receiving an item shows a message looked up in 143`Ancilla22_ItemReceipt .message` (`$08C2DD`, usdasm bank_08.asm:13233-13310). 144The bot cannot close one: `OPEN_CHEST` finishes once the item is received 145(`ap_plan.c:445-447`), the next task follows targets, and 146`ap_follow_targets` clears A on every frame it is not lifting or cutting 147(`ap_map.c:825`), so the text holds Link still until the goal times out and 148is permanently failed. Measured: a heart piece's message held the bot from 149frame 12660 until it gave up at frame 17237 ("No goals available; falling 150back to manual mode", `ap_plan.c:919`). 151 152Upstream's own 24-minute video (the `media` branch's `alttp_20200104.mp4`, 1531463 s at 60 fps, ~45 item checks) was sampled 20 times a second for a text 154box - a white border 25+ pixels tall at both of the box's side columns, 155which matches our own text-box frames - and has none (4 hits, all four 156castle walls on inspection): its Randomizer build shows no item text. The 157Randomizer's own source says the same: "Item pickup text is skipped by a 158change we will keep" (alttp_vt_randomizer app/Text.php:958, in 159`removeUnwanted`, which also blanks the escort messages). 160`zbanks::rando::PATCHES` makes vanilla show none either, in all three places 161it would: 162 163| USA address | Vanilla | Patched | What | 164| --- | --- | --- | --- | 165| `$08C2DD` | one message id per item, 0x4C words | all `$FFFF` ("no message", bank_08.asm:13773-13775) | `Ancilla22_ItemReceipt .message` (bank_08.asm:13233-13310) | 166| `$08C380` | `$0155 $0156 $0157` | `$FFFF` ×3 | `.heart_piece_message`, a heart piece from a chest (bank_08.asm:13322-13326, 13754-13756) | 167| `$05F0BC` | `JSL ShowMessageUnconditional` | 4 × `NOP` | a heart piece lying on the ground (`Sprite_EB_HeartPiece`, bank_05.asm:20413-20422) | 168 169The pendant message after all three pendants (`.pendant_message`, 170bank_08.asm:13722-13733) is left alone: the bot is nowhere near it yet. 171 172### The Kakariko informant: a box the bot walks into (not a Randomizer difference) 173 174In Kakariko, while the game state is 2 (which open mode sets), two 175informants wander: the young and the old "snitch", sprites `$34` and `$3D` 176(state-2 sprite list, usdasm bank_09.asm:15742-15755). Touching one runs 177`Snitch_Meander`'s `JSL ShowMessageOnContact` with message `$2F`, "Here is 178[LINK], the wanted man! Soldiers! Anyone! Come quickly!" (`$05E776`, 179bank_05.asm:18772-18789; text.asm:1315-1318), then `CallThePolice`. 180 181The Randomizer keeps it: `kakariko_alert_guards` is retexted ("Guards! 182Help! The creeper @ is here!", alttp_vt_randomizer app/Text.php:167) and is 183not in `removeUnwanted`'s `{NOTEXT}` list (Text.php:931-1127); z3randomizer 184touches neither sprite. So upstream could meet the same box. Why its run 185never did: the bot treats both informants as obstacles (`$34` BLKF, `$3D` 186BLKS, ap_snes.h:528, 537) and paths round where they stand; the box opens 187only if one walks INTO Link. Upstream's video is in Kakariko four times 188(1220-1232 s, 1268-1275 s, 1321-1333 s, 1351-1367 s) and at 1354 s leaves 189the chicken house one tile from the old informant and steps round her - 190never touched. Ours was walked into (run `s1map`, frame 22,907: the young 191informant moves west into Link on a `GOTO_POINT`). The bot does try to close 192dialog - `ap_task_evaluate` mashes A in module `$0E` (ap_plan.c:357-366) - 193but the movement task then calls `ap_follow_targets`, which clears A on the 194same frame (ap_map.c:825): 11,648 "Mashing dialog" lines, then manual mode. 195 196Host-level fix, two `rando::PATCHES` rows: the `JSL ShowMessageOnContact` 197becomes `JSL CheckDamageToLink_same_layer_long` (`$06F129`), as for uncle - 198the same carry on contact, so he still calls the guards and runs - and the 199`TYA : STA $0DE0,X` after it goes, because the contact test leaves `$0E40,X` 200(`$82`) in Y where the message routine left a facing (bank_06.asm: 20106F16F-06F172). This removes a text box, not a behaviour. Measured: run 202`k2` (home remade from the patched ROM, `iter2`'s map - which is what 203`s1map` imported, not `iter3`'s as this page once said) matches `s1map` 204frame for frame through 22,000; at 22,907 the informant touches Link and a 205guard (`$45`) spawns in both runs; `s1map` sits in the box to manual mode, 206`k2` walks on and is out of Kakariko by 25,000 (`run/zbanks-c/k2/ 207frame-023000.png`: informant, guard, no box). 208 209### Cheats 210 211`ap_tick` itself writes, every frame, full health, 10 bombs, 10 arrows, the 212hammer and the lamp (`alttp.c:20-26`). They are the bot's code and run 213unchanged; the host's only part is carrying the writes back into the 214console. Vanilla has no "infinite" setting; these writes are it. 215 216### No map file 217 218`ap_tick` imports `map.19.txt` on its first frame (`alttp.c:31-38`): a map 219of screens, nodes and tile attributes exported by an earlier run 220(`ap_map_export`, `ap_map.c:3471-3499`). It is not in the repository 221(upstream never committed it), and `ap_map_import` returns quietly when the 222file is absent (`ap_map.c:3502-3503`). Upstream's video starts with goals 223the bot could only know from that map (it heads straight for uncle's `NPC` 224goal on leaving the house); ours starts knowing nothing and explores. 225Nothing is substituted for it; as upstream did, a run's `map_export.txt` 226seeds the next (`--import-map`). 227 228### The starting state 229 230`ap_init` loads the Snes9x savestate "home", then "hpegs" (`ap_snes.c:66, 23181`). Neither exists here. The bot never presses anything outside the play 232modules - `ap_tick` zeroes the pad and returns for any other module 233(`alttp.c:64-99`) - so it cannot get itself from power-on to a game. 234`apps/zbanks --make-home home` makes the equivalent of upstream's "home": 235create file 1 from power-on the vanilla way, apply the open-mode preset to 236the save, boot again, start file 1 (choosing Link's house at the spawn 237select), and keep the machine once Link stands in his house and can move. 238"hpegs" stays absent, so that load fails as a missing file would in Snes9x. 239 240## The stairs: a regression in upstream's last commits (carried patch 0001) 241 242Why ours never got the sword, found 2026-09-21 afternoon. The bot did reach 243uncle's room (`$55`, the secret passage, by its east-side stairs, entrance 244`$32`) - and then climbed its intra-room staircase and walked straight back 245down, for thousands of frames, until the goal was failed for good. Traced 246frame by frame (`ZB_TRACE`, and gdb on the static target list): 247 248- the bot pathfinds a target every 8 pixels up the column, straight across 249 the stair tiles (attribute `$3D`, walkable, `ap_snes.h:264`); 250- stepping on the stairs starts `Module07_10_SouthIntraRoomStairs`, which 251 carries Link ~40 pixels in ~60 frames and keeps its own step in `$B0` 252 (usdasm bank_02.asm:2926-3008); 253- `ap_tick` returns early whenever `$B0 != 0` (alttp.c:78-82, "load in 254 progress"), so the stairs case two lines below it - "Stairs; keep 255 following targets" (alttp.c:85-89) - never runs; 256- at the top the next unconsumed target is back on the stairs (Link at 257 y `$0B8E`, target `$0BB0`; `ap_follow_targets` looks only one target 258 ahead, ap_map.c:750-763), so the bot presses DOWN, rides the stairs down, 259 presses UP, and so on. The same loop is the one seen earlier in cave 260 `$10A`. 261 262Upstream's video does not do this, and its own history says why: the video 263was recorded at commit 7add0e6 (2020-01-04), and the `$B0` early return 264arrived in b9f7eee (2020-02-02, "Can almost clear aghanim's tower"; `git log 265-S"sub_submodule_index != 0x00" -- alttp.c`). The video's info line agrees: 266through the climb it reads "Plan: NPC via GOTO_POINT (517)", never the 267"main: 0x7; sub: 0x10; subsub: 0x1" the early return writes. So bcb2537 268itself, on any host, walks back down; nothing in save, ROM or map can undo 269it. `third-party/c/patches/0001-follow-targets-on-intra-room-stairs.patch` 270exempts the two intra-room stair submodules (`$08`, `$10`, both stepping 271`$B0`: bank_02.asm:2851, 2916, 2947) from the early return, which is exactly 272what the stairs case after it was written for. Confirmed before patching by 273forcing one re-path at the top in gdb: the bot then walked to uncle. 274 275## Eastern Palace `$B8`: the big key room (carried patch 0003) 276 277Run `s2a` stood in room `$B8` from frame ~36,400 to its end at 50,000. Not a 278locked door: Link was pinned under the room's anti-fairy circle 279(`s2a/frame-040000.png`). What the room is (usdasm): the circle 280(`Sprite_82_AntifairyCircle`, bank_1E.asm:12749-12799, HP 255, 281bank_0D.asm:6184) orbits (0x180, 0x0D0), the very spot of the pot the floor 282switch is under (bank_01.asm:18537); the room's tag `$27` is "switch makes 283the chest appear" (ap_snes.h:1029), and that chest holds the Eastern 284Palace big key (ITEMGET `$32`, bank_01.asm:19720). The circle breaks up into 285roaming anti-fairies (sprite `$15`) only once `CheckIfRoomIsClear` passes - 286the Eyegore and Stalfos dead. So vanilla's big key needs the room cleared 287first. 288 289What upstream's bot does with it: `$82` is a plain `SPRITE_ATTR_ENMY` 290(ap_snes.h:611), which the path planner only costs softly (ap_map.c: 2911192-1201), so paths to the pot and to the east door run through the ring 292and Link is knocked back every attempt ("Link state: 0x2"; ~20 timeouts on 293`L:8380,16f0; 83c0,1680`); the tag `$27` handler walks to the switch 294(ap_plan.c:298-316) and never kills anything; and `SCRIPT_KILLALL` targets 295the first active sprite (ap_plan.c:659-667), which the ring's sprites are - 296it would swing at them forever. 297 298Why upstream never met it: it is the same room in the Randomizer, which 299moves the big key but not the room's sprites. Upstream's video (sampled at 3001 fps, 700-1030 s) opens the Eastern Palace big chest at ~816 s without 301ever entering `$B8`: its seed's big key was elsewhere. Ours is here, and 302`s2a` had already met the big chest without it at 33,000. 303 304Why a carried patch and not the host: there is no host difference to undo 305(same room, same sprites, same save), and the only thing a host could 306change is the room - deleting the circle or the enemies from the cartridge 307- which is changing the game rather than hosting it. The informant patch 308above removes a text box and keeps what he does; this would remove a 309puzzle. `0003-anti-fairy-circle.patch`, in upstream's own idiom: 310 3111. `$82` becomes `ENMY | NVUL | BLKF` (invulnerable, blocking), and `$15`, 312 the anti-fairy it breaks into, `ENMY | NVUL` - the game does not count 313 them when it decides a room is clear either. 3142. Kill-all skips `NVUL` sprites. 3153. A `SCRIPT_KILLALL` entry, "EP clear big key room", at (0x8380, 0x1730) 316 below the circle, in upstream's per-room script table (ap_map.c:36-), 317 beside its "HC kill guard for key". 3184. Kill-all no longer fails its goal on the first frame an enemy has no 319 path to it (A* fails for a Stalfos beside a pot): it tries the others, 320 then waits 8 frames and looks again, and fails after 32 such misses. 321 Without it run `b4` permafailed the new script within two frames 322 (three "ap_pathfind_sprite failed" at 37,045-37,046). 323 324Measured, Jev off, home remade from the patched ROM (`states3`), `s1map`'s 325map - the same start as `s2a`: `b1` (part 1 only) no longer pinned but the 326switch pot unreachable, so the chest goal stays unsatisfiable and the bot 327leaves; `b3` (1-3) clears the Eyegore and Stalfos, then swings at the 328dispersed `$15`s; `b5` (all four): script from 37,045, "Done killing" at 32940,155, pot `0x72` lifted and the switch found at 40,571, the big key chest 330opened at 40,702, the big key door north out of `$B8` at 41,241 and the 331one in the big chest room at 42,280 (`b5/frame-041000.png`: the chest open 332on the dais, anti-fairies roaming). 333 334## The Armos Knights: the bow shot cancelled (carried patch 0004) 335 336Run `j9` fought the Armos Knights (room `$C8`, upstream's own "EP Armos 337Knights Boss" kill-all, ap_map.c:152-158) from ~+16,500 to its end and 338killed one of six. Measured before changing anything, from `j9`'s last 339frame as `home` (`run/zbanks-c/states-armos`), `j9`'s map, Jev off: run 340`a0`, 12,000 frames, **not one arrow** in the log's ancilla dumps and five 341knights at HP 48 throughout; Link walked into them and was knocked back 342("Link state: 0x2"). Not sword level, not the bow missing (equipped at the 343fight's start, "SET_INVENTORY start item: BOW"), not knockback as such: 344 345- The knights are `SPRITE_ATTR_VBOW` (ap_snes.h:561), so kill-all fights 346 them with the bow: on each retarget frame it presses Y 347 (`JOYPAD_MASH(Y, 0x08)`, ap_plan.c) and suppresses the sword 348 (`no_sword`), then follows targets toward the knight. 349- `ap_follow_targets`'s last branch, when the next tile needs no lift, cut 350 or hammer, is `JOYPAD_CLEAR(A); JOYPAD_CLEAR(B); JOYPAD_CLEAR(Y);` 351 (ap_map.c:835-839). `ap_tick` zeroes the pad every frame (alttp.c:64), so 352 the only Y it can ever clear is one the caller set just before: the bow 353 shot, cancelled on the frame it was pressed. 354- That branch arrived in upstream's LAST commit, bcb2537 (2020-02-06, 355 "Hammer/sword/lift obstacles in path"; `git log -S'JOYPAD_CLEAR(Y);' -- 356 ap_map.c`). Upstream killed the Armos Knights at 8f13023 (2020-01-02, 357 "Kill first boss: Armos Knights") and recorded its video at 7add0e6 358 (2020-01-04) - both with the same kill-all code and no such line. The same 359 story as the stairs: a regression in the last commits, which no host can 360 undo, because the Y is set and cleared inside one `ap_tick`. 361 362`third-party/c/patches/0004-bow-shot-survives-follow-targets.patch` drops 363the `JOYPAD_CLEAR(Y)` and nothing else. Same start, patched (run `a1`): 62 364arrow ancillae logged, all five knights dead and "Waiting for crystal to 365drop" at 3,000, the pendant held up at 3,500 (`run/zbanks-c/a1/ 366frame-003500.png`), then out of the boss room to the dungeon's remaining 367chests. 368 369## Goals given up too early come back (host) 370 371Run `b5` tried the Eastern Palace big chest four times at ~33,000 - before 372the big key - and never again: at 40,702 it had the key, and at 51,780 it 373gave up the game (manual mode) in front of the red Eyegore room, which needs 374the bow the chest holds. From source (bcb2537): 375 376- `ap_goal_fail` (ap_plan.c:917-926) counts an attempt and past three 377 `LL_EXTRACT`s the goal - off the list for good, never freed. Nothing puts 378 one back: not item pickup, not requirement changes, not map import (which 379 only creates fresh goals, once per process). `git log -S` over upstream's 380 whole history finds no reset of `attempts` ever. 381- The bot does not know a big chest needs the big key: `chest_type = 1` is 382 set for big chests (ap_map.c:3233-3238) and never read; no big-key 383 requirement is put on `GOAL_CHEST`; `ap_node_islocked` checks the big key 384 only for key blocks and big-key doors. So the chest looks open, `OPEN_CHEST` 385 presses A, nothing arrives, the task times out, and after four the goal 386 is gone. 387- Upstream's TODO still lists "Reset goal attempts count when re-visiting 388 screens (maybe a bad idea?)" (added 488a6cf, 2019-12-01; "(maybe a bad 389 idea?)" appended in ceb3ac0, 2019-12-31; never done). There are no commits 390 after bcb2537 on any branch but the video commit. Its Randomizer run never 391 met this because that seed's big key was not in `$B8` (see "Eastern 392 Palace `$B8`"); its only "reset" was restarting the process. 393 394**Host fix, not a patch.** The shim watches the goal list after every tick 395(`zb_watch_goals`): a goal that has left it with `attempts > 3` was 396permafailed (a completed one leaves with at most three). When Link's 397possessions change - items, keys, big keys, pendants, crystals, progress 398(`zbanks::POSSESSIONS`), never rupees, health or arrows - every given-up goal 399is put back through the bot's own `ap_goal_add`, fresh, and the bot's log 400says so and why. Re-visiting screens (upstream's idea) is the wrong trigger: 401a goal that fails for a reason a visit does not change would be retried 402forever. A change in what Link has is the thing that can make a failed goal 403succeed, and it bounds the retries: four attempts per change. `--no-retry` 404is upstream's behaviour. 405 406Measured, Jev off, `b5`'s exact start (`states3` home, `s1map`'s map), with 407this and carried patch 0004 (run `r1`, 70,000 frames): the big chest is 408permafailed at 33,075 exactly as in `b5`; the next small key 409("`$7EF36F 00->01`") puts it back; big key 40,702; **big chest (the bow) 41042,146**; "EP Igor Key" 44,133; "EP Red Igor Room" 48,095 (where `b5` gave 411up); **Armos Knights 54,080, the pendant on the floor at 54,000** 412(`r1/frame-054000.png`); two more chests; still playing at 70,000. 413 414## Starting mid-game: goals the save says are done (host) 415 416Upstream always started from its "home", a fresh save, so every node its 417imported map recreated was still to do. We also start mid-game - the 418window's `resume`, a later headless run's `home` - with the map the bot built 419imported, and then an imported NPC can be done already. Run `o1` (from 420`r1`'s last frame, `r1`'s map, Jev off) walked back to uncle's spot in the 421secret passage for the NPC goal the import recreated, and died at frame 42221,758: uncle is gone once `$7EF3C6` bit 0 is set (`SpritePrep_Uncle`, 423usdasm bank_05.asm:16216-16240); `TALK_NPC` takes a nonzero `$02D8` - still 424the last item Link received - as uncle's gift (ap_plan.c:483-487); and the 425item tracker, which already has uncle's location down as the sword 426(ap_item.c:492-495), aborts on `assert(item_loc->item->type == item_type)` 427(ap_item.c:518). In the window that abort would close the window. 428 429Chests and pots the bot re-checks against the save itself (`sram_room_state`, 430the tile under a pot); an NPC it cannot. `ap_map.c`, where every goal is 431made, is compiled with `-Dap_goal_add=zb_goal_add` (`third-party/c/BUCK`, 432the same mechanism as `-Dfopen=zb_fopen`), and the host's `zb_goal_add` 433declines an NPC goal for uncle when `$7EF3C6` bit 0 is set, then calls the 434real `ap_goal_add`. Run `o2`, same start: "not adding goal for sprite 4350x73.0x100: done in this save", no abort, 83,580 frames. 436 437## Sahasrahla: the green pendant is bit `0x04` (carried patch 0005) 438 439`o2` then played 83,580 frames after the pendant without once going to 440Sahasrahla, and gave up. His NPC goal requires `REQUIREMENT_GREEN_PENDANT` 441(ap_plan.c:133-136), which the bot sets from `$7EF374 & 0x01` 442(ap_req.c:58). The Eastern Palace gave `$7EF374 = 0x04` (`o2/progress.tsv`, 443`pendants`). The layout is "- - - - - g b r" (z3randomizer sram.asm:139; 444jpdasm symbols_sram.asm:779-781): `0x01` is red, `0x04` green. Vanilla 445Sahasrahla tests `AND.b #$04` (usdasm bank_05.asm:20560-20562), and so does 446the Randomizer (dialog.asm:355) - upstream's `0x01` is wrong in both games, 447and its seed must simply have met it some other way. The only user of the 448requirement is that goal. No host fix: the bot reads the byte itself. 449`0005-green-pendant-is-bit-4.patch` tests `0x04`. 450 451## The boots: walking up to Sahasrahla, and talking until he gives (carried patches 0006-0008) 452 453With patch 0005 the goal was chosen (run `o3`, 55,233) and failed, and each 454fix exposed the next step of the same errand. Every one was isolated by 455replaying `o3` exactly (Jev off replays frame for frame) to Sahasrahla's 456door at 59,300 (`--save-at`) with the change gated on `ap_frame >= 59300` - 457a throwaway build (runs `o3c`), since an ungated pathing change moves the 458whole run from its first frame. 459 4601. **No path to him.** "A* failed, min heuristic: 2" with Sahasrahla standing 461 there (sprite 0, `0x16.0 BLKF|TALK|NODE`). `ap_pathfind_local` makes a 462 blocking sprite's hitbox plus one 8-pixel cell impassable (ap_map.c:1204- 463 1231); a sprite node's box is the sprite's own box 464 (`ap_map_add_sprite_nodes_to_screen`); the goal test needs a cell inside 465 it; the margin rings it. Unblocking only the box's cells was not enough 466 (the hitbox sticks out of the box). Fix: the sprite being walked to does 467 not block that search. 4682. **Never "there".** `XYLINKIN` wants Link's whole 16x16 box inside the 469 node box, and his body is solid: Link stopped at (6a78,2190), 8 pixels 470 short, "Stuck Link Detected", timeout. Fix: at a talking sprite, within 8 471 pixels is arrived - `TALK_NPC` presses up and A from there, as upstream 472 wrote it. (Upstream's own earlier answer, a node placed below the sprite, 473 is commented out at ap_map.c, from 072b195.) 4743. **A false gift.** `TALK_NPC` counted any nonzero `$02D8` as the NPC's 475 item. `$02D8` keeps the last item Link got (`0x17` here), so the talk 476 "succeeded" in one frame and left the text box it had opened with nothing 477 pressing A - the same stale read that made `o1` abort at uncle. And the 478 real gift is received while Link holds it up, when `ap_tick` does not 479 evaluate tasks at all (alttp.c:126-127); by the next 480 task frame `$02D8` is 0. Fix: a gift is a change in what Link has 481 (`$7EF340-$7EF35F`, less the bomb count) since the talk began. 4824. **The talk timed out mid-text.** Sahasrahla's text outlasts `TALK_NPC`'s 483 64 frames, and a goal failed mid-text left the box open. Fix: while a 484 text box is up, the talk does not time out. 4855. **Walled in afterwards.** Link ends pressed against him, inside his 486 margin, and every search from there failed: every goal unsatisfiable, 487 manual mode at once. Fix: a sprite whose margin Link already stands in 488 blocks only its own cells. 4896. **Pushing along his body.** The first step of each new path was a 490 4-pixel sidestep into his solid body (69 "Stuck Link"). Link can walk 491 away (checked with `ZB_HUMAN=100-160:0x0440`, X+down: y 0x2188 to 0x21d8 492 in 60 frames). Fix: when the talk is done, step down off the NPC for 20 493 frames. 4947. **A dash in a text box.** With the boots, the A that closes his last 495 text box starts a dash (`$5D = 0x11`, `kPlayerState_StartDash`, zelda3 496 player.h:21; the bot's enum calls `0x11` `FALLING_LEDGE`), `ap_tick` only 497 plans - and so only mashes dialog - on the ground, and the box stayed 498 open for good (`o3c/frame-065000.png`). Fix: an open text box gets A in 499 any state. 500 5011, 2 and 5 are `0006-walk-up-to-a-blocking-npc.patch`; 3, 4 and 6 502`0007-talk-to-an-npc-until-it-gives.patch`; 7 503`0008-text-box-gets-a-in-any-state.patch`. With all of them, gated (last 504`o3c`): the boots at ~60,500, out of the hut, exploring for 40,000 frames 505(areas `$2C`, `$34`, `$18`, `$1B`, the Eastern Palace again) until manual 506mode at 100,620. 507 508**What stops it next: the Book of Mudora.** It sits on the library's 509bookshelf and falls only to a dash into the shelf. The bot has no dash: 510nothing in it holds A to run; the boots appear only as a requirement on 511`TILE_ATTR_BONK` explore goals (ap_plan.c:142-143). The Desert Palace 512entrance needs the book (`door 0x09`, ap_plan.c:152-155), so the bot's goal 513system has nothing past this point but what it has already done. 514 515## The castle passage door: a second stairs-shaped regression (fixed 2026-09-21, patch 0006) 516 517Found 2026-09-21 night: every fresh `apps/zbanks` run from the canonical 518`home`/`hpegs` states froze at exactly `link_x,y = 0x0500,0x0277` in room 519`$0012`, the small connecting room right past the Hyrule Castle secret 520passage's door (`door D 0x8e`, screen `0x0250`). `GOTO_POINT`/`TRANSITION` 521toward that door repeated forever, position frozen, until the goal 522permafailed and the run gave up without the sword - the same shape as "The 523stairs" above: a place the bot reaches every time and never crosses. 524 525**Bisected, not reasoned about**, by rebuilding at each commit between the 526last known-good run (`j9`, 15:52-15:55, HEAD then was `8a32e63d`) and HEAD, 527and replaying the identical canonical `home`/`hpegs` (made once at 15:55 and 528untouched since) headless for 45,000 frames each time: 529 530| Commit | Result | 531| --- | --- | 532| `8a32e63d` (j9's HEAD) | No freeze; explores to Kakariko, gives up on an unrelated pot puzzle at frame 34,883 (pre-existing, not this bug). | 533| `ee2cf60a` (patch 0005 + the mid-game-uncle host fix) | Same as above, frame 36,212. Not the regression. | 534| `33246459` (patches 0006-0008, Sahasrahla's boots) | Frozen at `0x0500,0x0277`, room `$0012`, gives up at frame 32,359. | 535| `4ea2b00b` (HEAD) | Same freeze, same frame, 32,359. | 536 537`33246459` is a direct child of `ee2cf60a` - one commit, three patch files. 538Rebuilding with only `0006-walk-up-to-a-blocking-npc.patch` present (0007 539and 0008 removed) reproduced the exact same freeze; 0007/0008 are not 540involved. 541 542**The cause: patch 0006's `ap_map.c` hunk is scoped to a coordinate overlap, 543not to the sprite it was written for.** The hunk changes the loop in 544`ap_pathfind_local` that marks every `BLKF`/`BLKS` sprite's hitbox 545impassable - generic code, called for every pathfind toward every kind of 546destination (a door, a chest, a sprite). Its `destination`/`inside` guards 547were meant to stop a TALK sprite's own margin from blocking the search for 548*itself* (Sahasrahla, `0x16`), by comparing that sprite's hitbox against 549`dst_tl`/`dst_br`, the CURRENT call's destination region - but with no check 550that the sprite being tested is the thing the search is actually walking up 551to. Room `$0012` holds a `BLKF` sprite (type `0x73`, the same template type 552uncle uses in room `$55`, but not `TALK`-attributed here - 553`ap_snes.h`'s per-type override at `.type = 0x73, .subtype = 0x0100, 554.only_dungeon_room = 0x55` only adds `SPRITE_ATTR_TALK` inside uncle's own 555room) that happens to overlap the door's destination box for an ordinary 556`door D 0x8e` pathfind. Patch 0006 made that sprite transparent to the 557SEARCH for that pathfind; it stayed solid to the real engine, so the 558computed path walked Link into it and the real collision held him there 559forever - exactly what a search that no longer treats an obstacle as an 560obstacle produces. 561 562**Fix**: gate both `destination` and `inside` on `SPRITE_ATTR_TALK` 563(`ap_sprites[i].attrs & SPRITE_ATTR_TALK`), so the exemption only ever 564applies to a TALK sprite - the one case 0006 was written for. Sahasrahla 565(`0x16`) has `SPRITE_ATTR_TALK` unconditionally, so this is a no-op for the 566scenario 0006 fixes; room `$0012`'s non-TALK `0x73` (and any other 567non-talking blocking sprite that happens to sit near a destination) stays 568solid to the search, as the real engine has it. 569 570**Measured with the fix**, headless, canonical `home`/`hpegs`, `s1map`'s 571imported map (Jev off), 90,000 frames: no freeze at `0x0500,0x0277` (0 572occurrences, against hundreds before the fix); through the passage and past 573room `$0012` by frame 3,000 with the **sword**; **bow** (the Eastern Palace 574big chest) at frame 42,600; **green pendant** (`$7EF374 = 0x04`) at frame 57554,600; **Sahasrahla's boots** at frame 58,200 - all within the range of the 576best pre-regression runs (`j10`: sword 2,719, bow 43,714, pendant ~59,000, 577boots 62,499) - and still playing, sword+bow+pendant+boots all held, no 578manual mode, at frame 90,000 (fighting through a `SCRIPT_KILLALL` and a 579hole drop in dungeon room `$0123`), further than any pre-regression run got 580(`j10`/`v1` both gave up between 83,280 and 86,760). 581 582## The window's stall: not the Armos Knights, a real physical wedge in room `$0012` (2026-09-21 night) 583 584The live window sat in manual mode for over an hour, reported as "the 585Eastern Palace Armos Knights room" from its own `bot` MCP tool's task list 586ending in `SCRIPT_KILLALL ... EP Armos Knights Boss`. That task list is a 587STACK, not the current position: `read_wram` and `state` over MCP showed the 588true, live position - `link_x,y = 0x0500,0x0277`, room `$0012`, the small 589room just past the Hyrule Castle secret passage's own door (`door D 0x8e`, 590screen `0x0250`) - the Armos task was queued, unreached, twenty-some steps 591down a plan that never got past its first one. `$7EF374 = 0x04` (the green 592pendant, read live) proved Eastern Palace was already cleared long before 593this stall; the boss-room framing was a misreading of the task stack, not 594the ground truth. 595 596**Reproduced headless** from a copy of the window's own `resume.state` 597(made its own `home` in a scratch states directory): the SAME freeze, 598same position, at both `main` (patch 0006 fixed) and `ee2cf60a` (before 5990006 ever existed) - ruling out patch 0006 as the cause here, unlike the 600castle-passage regression above. 601 602**Proven a real physical dead end, not a search bug**, with `ZB_HUMAN` 603(needs `apps/zbanks` built from a commit that has it, `>= 33246459` - 604testing this at `ee2cf60a` first gave a false "nothing moves" result 605because that commit's `main.rs` does not read `ZB_HUMAN` at all and passes 606`0` to every tick regardless, `apps/zbanks/src/main.rs:151` on `main` 607vs. the hardcoded `0` at the same call on `ee2cf60a` - always rebuild from 608the commit under test): holding UP, DOWN, LEFT, RIGHT and A alone for 200 609frames each, raw pad, no bot in the loop, moved Link **zero pixels** in 610every case. A frame-by-frame trace (`ZB_TRACE`) during the A-alone test 611showed why: with the Pegasus Boots already owned (`$7EF355 = 1`), A with 612nothing to interact with starts an automatic dash charge 613(`link_state = 0x11`, `kPlayerState_StartDash` - see "The Book of Mudora" 614above), and the charge itself never resolves either - it sits in 615module `$0E` sub `$02` for 800+ more frames. Two independent room-relative 616sprite dumps of `$0012` (this session's and an earlier one from before any 617of tonight's patches) agree: Sprite 0 (`type=0x73, subtype=0x200, BLKS`) 618and Sprite 1 (`type=0x76, subtype=0x200, BLKF|FLLW` - Princess Zelda's own 619sprite type, `ap_snes.h:598`) sit within single-digit pixels of Link, and 620upstream's own `ap_snes.h` has a ROOM-SPECIFIC override baked in for this 621exact room - `{ .type = 0x73, .subtype = 0x0000/0x0100, .only_dungeon_room 622= 0x12, .attrs = SPRITE_ATTR_BLKF }` (`ap_snes.h:748-749`) - meaning 623upstream's own authors already knew room `$12` puts blocking sprites where 624a generic type `0x73` elsewhere would not be one. A live screenshot 625(`frame` MCP tool) shows why: a small vestibule, a carved idol centred at 626the top, and Link boxed between two flanking figures with no visible gap 627on either side. 628 629**Not a resume/goal-state bug and not fixable in the C bot's own logic**: a 630fresh, continuous walkthrough of this same room (this session's own 631`verify-0006-fix`/`verify-main-fix` runs, 20,000-90,000 frames) crosses it 632without incident every time - the wedge is a property of the EXACT pixel 633Link was standing on when something (a periodic `resume` save, or genuine 634natural play) left him there, not a property of the room itself. Since raw, 635un-bot-mediated input in every direction fails identically, no pathfinding 636or goal-retry change can reach a cell that plain movement cannot leave - 637this is a real ALTTP engine dead end, the kind a human player escapes by 638power-cycling the console. 639 640**Recovery**: `power_cycle` (dev-mode MCP tool; `packages/console`'s 641`hard_reset`, which reloads the cartridge's own SRAM, so items are not at 642risk) escapes the position but lands at the title screen, and this specific 643attempt could not navigate back through it - `Module14_Attract` 644(`~/src/github.com/snesrev/zelda3/src/attract.c:372-386`) only accepts 645Start/B during particular `attract_state` values (not 0, 2 or 6) and while 646`INIDISP_copy` is set, and mashing Start on a fixed cadence for several 647thousand frames never landed in a window where the demo yielded to input on 648this run - not chased further, since `load_state("home")` (a name already 649kept beside the ROM, and confirmed compatible with the current patched ROM 650CRC where two older named checkpoints, `has-sword`/`outside`, were not - 651"snapshot is of cartridge 777AAC2F, this is 173FFEC8", made before a later 652ROM-patch change) reaches an ordinary, controllable, playable state directly 653with no menu navigation at all. Cost: the session's sword/pendant/boots 654progress, traded for an unstuck, playing window - the alternative was 655leaving it in either the original wedge or a half-navigated demo loop. 656 657**A second, smaller bug found and fixed along the way**: `apps/native`'s own 658stall detector (`apps/native/src/stall.rs`) is correctly a one-way latch per 659its own doc comment ("a `restart` makes a fresh bot ... and should make a 660fresh `Detector` alongside it, so a stall from the last process never 661survives into the next one's frame 0") - but nothing enforced the other 662half of that sentence. `check_stall` only writes or removes `stalled.json` 663**on a transition** (`app.stalled == was` is the whole guard); a fresh 664process whose own `Detector` starts at `None` and stays there all its life 665never transitions, so a `stalled.json` left on disk by a PREVIOUS process 666(this session's own restarts included) is never touched and reads as a live 667stall indefinitely. Fixed (`apps/native/src/app.rs`, right after 668`bot_dir` is created, before `zbanks::Bot::start`): remove any 669`stalled.json` unconditionally on every startup, since a fresh Detector's 670ground truth is always "not stalled" at frame 0 and the file should say so 671immediately rather than carry forward a description of a process that no 672longer exists. 673 674## The Book of Mudora (carried patch 0009, mechanism only - not yet wired) 675 676Confirmed there is no dash anywhere to steal: `git log -S dash|Mudora|bookshelf` 677over upstream's whole history (`8192fb7`..`bcb2537`) finds `BookOfMudora`, 678`BONK`/`TILE_ATTR_BONK` and the dash comment in `ap_snes.h` unchanged since 679they were first written, and no code that ever presses A to run. Sprite `$3B` 680("BonkItem", which the book, a hidden key and a fake tree all share via a 681sub-jump on `$0DC0,X`) has `attrs = 0` in `ap_snes.h:535` - invisible to the 682map/goal system entirely, not merely ungated. 683 684The game's own mechanism, from usdasm and zelda3 (snesrev's C port of the 685disassembly, `src/player.c`): pressing A with the boots equipped and nothing 686else to interact with always attempts a dash (`Link_HandleLiftables`, 687`kAbilityBitmasks[2] = 4`, the boots bit) - not a location-specific move. 688`Link_PerformDash` sets `kPlayerState_StartDash`; `LinkState_Dashing` then 689requires **A held on every frame of a ~29-frame stationary charge** 690(`if (!(joypad1L_last & kJoypadL_A)) { ...cancel... }`, checked while 691`link_countdown_for_dash` counts down) - a single dropped frame cancels it. 692Once the charge ends Link moves under his own power at dash speed and no 693longer needs A held; only a NEW, different direction stops him 694(`want_stop_dash`), which is why `0009`'s dash-hold script characters keep 695holding A anyway for the whole approach - harmless, and it removes any timing 696window at the charge/travel boundary. `BookOfMudora_WaitForBonk` 697(bank_05.asm:22825) then needs Link's position within a small anticipatory 698box of the sprite AND nonzero velocity (`$011A`/`$011C`) - i.e. genuinely 699dashing, not walking - before the shelf's own state machine (`WaitForBonk` → 700`KnockedDown` → `Land` → `GrantLiterature`, which calls `CancelDash_long`) 701runs; walking collision stops Link outside contact range of the item's own 702hitbox, so a plain walk can never even trigger it. 703 704Why the C bot could never do this from outside: `ap_follow_targets`'s own 705per-tile logic (`ap_map.c`, the branch that lifts pots and swings the hammer) 706ends in an unconditional `else { JOYPAD_CLEAR(A); ...}` whenever the tile 707ahead needs no lift/sword/hammer - which is every frame of a dash across open 708floor - so even a host that set A itself would have it erased the same tick. 709`0009-pegasus-boots-dash.patch` adds four `SCRIPT_SEQUENCE` characters (`8` 710`2` `4` `6`, numpad directions, chosen not to collide with the existing 711`<>^vABYUD` letters) that hold A together with a direction in ONE target 712(repeat the character to cover more distance; each target only pops once 713Link's real position reaches it, so it keeps re-asserting both bits for 714however many frames that takes - the charge, then the travel), and narrows 715the `JOYPAD_CLEAR(A)` to skip only when the *current* target holds A AND a 716direction together - true only for these new characters, never for the 717existing `'A'`/`'B'`/`'Y'` taps (which set only their own button), so no 718existing script's behaviour changes. Built, applies, and a 5,000-frame 719replay from `states3`+`s1map` (sword by 3,000, well/uncle/HC by 5,000) is 720unchanged from the pre-0009 shape. 721 722**Wired (carried patch `0010-book-of-mudora-goal.patch`), coordinate DERIVED 723from ROM data, not yet live-walked.** An `ap_scripts[]` entry now exists 724(`ap_map.c`) and `ap_goal_add` (`ap_plan.c`) requires `REQUIREMENT_BOOTS` for 725it by name, the same pattern as Sahasrahla's pendant check. Proven headless 726(`run/zbanks-c/` scratch runs, not kept - reproduce with `--import-map` from 727any map that has tagged the Library, e.g. `j10`'s `map_export.txt`): the 728script's node attaches to the right screen (`ap_map.c:ap_screen_add_raw_node` 729logs "attached node Script: Library Book of Mudora") and `goals.txt` shows it 730`unsat Needs: [BOOTS]` from a boots-less `home`, and `limit` (in range, not 731unsatisfiable - i.e. the requirement is satisfied and it is only a search-cost 732cutoff) from a boots-having save. The requirement gate is real; the 733IN-ROOM coordinates it dashes to are not yet proven against the real room. 734 735- The screen carrying the front door is already named in the bot's own 736 `ap_screen_infos[]` (`ap_map.c`): `{ .id = 0x0a02, .name = "Library Yard", 737 .add_explore_goals = true }` outdoors, `{ .id = 0x2178, .name = "Library" 738 }` for the interior (`ap_map.c`'s screen ids are `(y>>8)<<8 | (x>>8)`, 739 256px quadrants). Entrance table entry `0x49` (`~/src/github.com/JaredBrian/AsarUSALTTPDisassembly` 740 `Bank02.asm:11975` `Dungeon_LoadEntrance`, indexed at `Bank02.asm:11525` 741 `.rooms`/`Bank02.asm:11712` `.playerY`/`Bank02.asm:11731` `.playerX`, pinned 742 `e41fef7`) gives room `$0107`, `playerY=$21D8`, `playerX=$0E78` - stored 743 straight into `$20`/`$22` by that routine, matching `SpritePrep_DashItem`'s 744 own hardcoded library check (`room == 0x07, sub == 0x01`, same repo 745 `Sprites/sprite_prep.asm:1394,1398`, mirrored at 746 `walkingeyerobot/alttp-disassembly` `sprite_prep.asm:1394`). 747- **The indoor address space widens X but not Y.** `ap_snes.c:ap_sprites_update` 748 applies `x = ((x & ~0x1FF) << 2) | (x & 0x1FF); x += 0x4000` to a sprite's 749 hitbox X when `*ap_ram.in_building`, and leaves Y untouched. Applied to the 750 entrance's raw `playerX=$0E78`: `(0x0E78 & 0xFE00)<<2 | (0x0E78 & 0x1FF) = 751 0x3878`, `+0x4000 = 0x7878` - which lands inside the screen's own measured 752 tags (`Screen tags: … Library v 7800,2100 x 78ff,21ff`, logged by 753 `ap_map_add_constants_to_screen` in every run that ever referenced the 754 door, e.g. `run/zbanks-c/b3/bot.log:4718`). `playerY=$21D8` needs no 755 transform and is already inside the same tags' Y range. This is the 756 cross-check the earlier attempt at this (see history below) lacked. 757- **The sprite's own room-relative bytes, decoded with a control group.** 758 `RoomData_Sprites_Room0107` (`Bank09.asm:8050`): `db $15, $03, $3B` for the 759 book, and two decorative `$6D`s at `db $1B, $17, $6D` / `db $1B, $18, $6D`. 760 The disassembler's own comments read `xy: { 0x030, 0x150 }` for the book 761 and `{ 0x170, 0x1B0 }` / `{ 0x180, 0x1B0 }` for the decorations - and 762 `byte1*16` (`$03*16=0x030`, `$17*16=0x170`, `$18*16=0x180`) matches the 763 FIRST ("x") number both times, `byte0*16` (`$15*16=0x150`, `$1B*16=0x1B0` 764 twice) matches the second ("y") both times. So the book's room-relative 765 position is **X=`0x030` (48), Y=`0x150` (336)** - the two decorations 766 share a Y (`0x1B0`) and sit 16px apart in X (`0x170`/`0x180`), which is 767 exactly what two objects side by side on the same wall would look like, 768 and is why a single sprite's own bytes were not trusted alone. 769- **Room origin, and the book's absolute position.** Local offsets under 770 `0x200` pass through the X-widening formula unchanged (it preserves 771 `x & 0x1FF` verbatim), so an offset from the room's local origin maps 772 1:1 onto an offset from the room's WIDE origin. Taking `$7800,$2100` (the 773 clean quadrant boundary the screen tags name) as that origin: book 774 absolute = `(0x7800+0x030, 0x2100+0x150)` = **`(0x7830, 0x2250)`**. The 775 entrance spawn (`0x7878,0x21D8`) is `0x48` (72px) EAST and `0x78` (120px) 776 NORTH of the book - not the "shelf on the wall opposite the door" a 777 reflex north-dash-from-the-doorway would assume. `start_tl` is set to the 778 entrance's own Y but the book's X (`0x7830,0x21D8`), which ordinary 779 `GOTO_POINT` pathing (no dash) can reach from the door before the script's 780 ten `2` (dash-south) characters take the ~120px remaining, with margin for 781 the ~29-frame charge and the fact the box `WaitForBonk` checks is small 782 and anticipatory rather than pixel-exact. 783 784**Why this was not live-walked this session, and what actually blocked it.** 785Getting Link physically into the room needs boots (for the gate to matter at 786all) and a walk there, and every fresh `apps/zbanks --states 787"roms/….states"` run tried this session (no Jev, canonical `home`/`hpegs`, 788several independent attempts) **never got the sword**: it explores the 789reachable light-world screens that need no items (70+ EXPLORE goals 790completed, per `goals.txt`'s own `Stats:` line) and then gives up, because 791the one NPC goal ever created in any of these runs is not uncle's - the 792route down through the Hyrule Castle secret passage (`door D 0x8e`, screen 793`0x0250`, landing in room `$0012`) is walked to (position frozen exactly 794there, `link_x,y = 0x0500,0x0277`) but the transition never completes, 795across three-figure frame counts and multiple independent fresh attempts. 796`run/zbanks-c/j9` (this same day, **15:52-15:55**, i.e. before carried patch 797`0009` at **19:09**) got the sword and reached the Armos Knights from a 798`home`/`hpegs` that has not changed since (`home.state`'s own mtime, 79915:55:43, matches `home-before-informant-patch.state` exactly). The 800correlation with `0009`'s timing does not hold up under reading the code, 801though: `ap_follow_targets`'s new guard only changes behaviour when the 802CURRENT target's `joypad_mask` has both `A` and a direction bit set 803together, and every ordinary `GOTO_POINT`/`TRANSITION` target (as opposed to 804a script's) is built with `joypad_mask = 0` (`ap_map.c`, e.g. line 681, 805734, 1442) - so `0009` is provably a no-op on this path, and what actually 806regressed (if anything did, rather than this always being an occasional 807snag on this one transition) is not identified. The `ap_goal_fail` 808`assert_bp` (`ap_plan.c:968`, `ap_tick`'s own anchor + offset `0x147cc`, 809confirmed with `addr2line`) that both this stall and the LIVE WINDOW's 810separate, later Armos-fight stall share is the GENERIC one-per-codebase 811"a goal failed" tracepoint, not evidence the two are the same bug. 812**Next step for whoever picks this up:** `apps/zbanks` from a fresh `home` 813with `ZB_TRACE` bracketing the approach to `door D 0x8e` (screen `0x0250`, 814room `$0012`) - a frame-by-frame trace the way carried patches `0001`/`0004` 815were each found, since "reaches the same standing spot every time and then 816never crosses it" is exactly their shape. Once past it (or from any 817boots-having save that already is), a plain headless run with the map above 818imported should walk to `start_tl` on its own; whether the ten-`2` sequence 819actually lands the item is the one thing left to prove, by reading 820`$7EF34E` (the book byte) after. 821 822## How far it plays (measured 2026-09-21) 823 824Headless, `apps/zbanks`, from the open-mode `home` state (runs under 825`run/zbanks-c/`, gitignored): 826 827| Run | Host as of | Frames | What happened | 828| --- | --- | --- | --- | 829| `bed` | no ROM remap | 0 | Died on the chest-table assert, `ap_map.c:3201` - the Japanese-address finding. | 830| `r1` | remap, vanilla rain start | 30,000 | Out of the house, then stood in a hint soldier's text box for the rest of the run. | 831| `r2`/`r3` | + open mode | 30,000 | 12 places; the Dam's block-puzzle script solved; a heart piece's text held it from frame 12,660 until it gave up (manual mode) at 17,237. | 832| `r4` | + no item text | 30,000 | 24 places, reaching the castle yard and graveyard; still exploring at the end. | 833| `iter1` | same | 44,580 | 36 places, 11/19 chests, 9 pots, 3/3 scripts, a heart container; fell into a hole into room `$2F` and gave up at 40,980. | 834| `iter2`-`iter4` | + each imports the last one's map | up to 60,000 | The map grows run on run (explore goals known 155 → 200 → 287 → 367) and each run heads somewhere new (Kakariko, where the informant's "Here is G, the wanted man!" text held it - see "The Kakariko informant" above). | 835 836The window (`apps/native`), started from `home` with `iter3`'s map as its 837`map.19.txt`, walked to the Eastern Palace and in (room `$C9`) within about 838three and a half minutes: `run/window/zbanks-1.png` (the ruins, frame 8,648), 839`run/window/zbanks-2.png` (inside, frame 12,886), `run/window/zbanks-3.png` 840(the Stalfos room, running its `SCRIPT_KILLALL` with no sword, frame 17,372). 841 842What it never did in these runs: reach uncle for the sword - see "The 843stairs" above. With carried patch 0001 and the uncle-message patch 844(home remade from the re-patched ROM, `run/zbanks-c/states2`): 845 846| Run | Map imported | Frames | What happened | 847| --- | --- | --- | --- | 848| `s1map` | `iter2`'s | 40,000 | Sword from uncle by frame 12,000 (room `$55`); then Kakariko, where the informant's text holds it (manual mode). | 849| `s1fresh` | none | 40,000 | Explores caves and houses; never near the castle in this run. | 850| `s2a` | `s1map`'s | 50,000 | Sword by frame 5,000; the castle's key guard killed (`SCRIPT_KILLALL`, 10,526, 11,183); Eastern Palace at 27,000; the Stalfos room (`$A8`) cleared - "Done killing" at 31,448, its trap door taken at 31,774 (`run/zbanks-c/s2a/frame-031000.png`, `frame-032000.png`); then pinned under the anti-fairy circle in room `$B8` to the end (see "Eastern Palace `$B8`"). | 851| `s2b` | `iter4`'s | 50,000 | Sword by 14,000; Dam puzzle and castle key guard; into Eastern Palace by 41,000. | 852 853The bot's own behaviour, run unchanged, that these runs meet: a text box it 854did not open holds it until its goals time out (`ap_follow_targets` clears 855A); it can loop on a pair of intra-room stairs for thousands of frames (cave 856`$10A`, frames ~5,700-12,000: one frame of `DOWN` at the top re-enters the 857stairs, one frame of `UP` at the bottom does the same); `assert_bp` fires 858tens of times a run, always at the same site (`ap_tick` + 0x146bc); and when 859every goal is unsatisfiable it sets `ap_manual_mode` and stops for good 860(`ap_plan.c:915-918`). Upstream's TODO lists stairs, and its video ran from a 861map built over many runs. 862 863### With both blockers gone (measured 2026-09-21, evening) 864 865All from home remade from the patched ROM (`run/zbanks-c/states3`): 866 867| Run | Jev | Map | Frames | What happened | 868| --- | --- | --- | --- | --- | 869| `b5` | off | `s1map`'s | 70,000 | As `s2a` to 36,400, then through `$B8`: big key 40,702, the big key doors 41,241 and 42,280, the Eyegore key room ("EP Igor Key") 44,092, on to the red Eyegore room (`$D8`) - which needs the bow, and the big chest holding it had been permafailed at 33,075, tried four times BEFORE the big key. Manual mode 51,780. | 870| `j4` | on | `iter2`'s | 40,000 | The informant touches Link at 18,583 (a guard spawns), no box; the Kakariko well at 20,422, Blind's house puzzle 24,568. 22 questions, $0.000505. | 871| `j9` | on | `p1`'s | 36,000 | Started as `home` = `p1`'s frame 34,000 (`b5`'s road, Jev off, in the Eastern Palace). `$B8` cleared at +5,156, big key +5,633, **big chest (the bow) +6,890** - its goal fresh from the imported map, not yet permafailed - Eyegore key +9,687, the Armos Knights (`$C8`) from ~+16,500 to the end: one of the six killed ("EP Armos Knights Boss" is upstream's own kill-all script), the rest at full HP 48. No pendant. 25 questions, $0.000608. | 872| `j2`, `j7` | on | `s2a`'s, `s1map`'s | 108,000, 72,000 | Jev's picks send the bot round the overworld and Kakariko's houses first; neither reaches `$B8`. | 873 874### With patches 0004-0008 and the host's retries (measured 2026-09-21, night) 875 876Same start as `b5` (`states3` home, `s1map`'s map), 200,000 frames: 877 878| Run | Jev | What happened | 879| --- | --- | --- | 880| `v1` | off | Bow 45,000 (the retried big chest), pendant by 59,000, boots by 63,000; manual mode 86,760 - the book is next and needs a dash the bot does not have. | 881| `j10` | on, run limit $0.05 | Sword 2,719; big chest (bow) 43,714; Armos Knights 58,209; pendant by 59,000; Sahasrahla's boots 62,499; manual mode 83,280. 124 choices, 70 questions, 48 reused, 6 not asked, 0 throttled, 0 fallbacks; 577 input tokens a question; $0.001696 over 24.13 game-minutes: **2.90 questions per game-minute, $0.000070 per game-minute**. | 882 883From the window's own last state (its `resume` of 18:22, the old build having 884given up at frame 80,157 with no goal since 79,548), run `w3` on this build: 885goals chosen again from frame 0 (the big chest, four times - no big key in 886that save - then pots and "EP Igor Key"), out of room `$A9` by ~1,100, then 887a ledge drop into a shutter room with two Stalfos and every goal 888unsatisfiable at 1,431. That save's route is its own trap; the window was 889relaunched from `home` instead (see the register). 890 891Two things these runs found that are not blockers of the game but of the 892harness: the ledger's 30-questions-a-minute breaker is wall-clock, and two 893headless runs at ~5x real time plus the window tripped it (15:32:05; every 894later choice fell back until `paused.json` was cleared) - run one Jev 895headless run at a time; and in `j3` Jev kept picking a goal in cave `$123` 896for 17,000 frames until the bot gave up. 897 898## Jev at the goal choice (carried patch 0002, `packages/zbanks-jev`) 899 900The bot's one real decision is `ap_goal_evaluate` (ap_plan.c:887-925): 901score every goal (`ap_goal_score`, ap_plan.c:735-841 - path cost, +100 per 902failed attempt, +10,000 for anything but an NPC while swordless) and pursue 903the lowest. `third-party/c/patches/0002-goal-choice-hook.patch` adds 904`ap_goal_choose_hook`, called with upstream's pick, its score and 905`ap_goal_score`; NULL, the default, is upstream exactly. It cannot be done 906from the shim: the choice sits between two statements of a `static` 907function. The shim's hook (`zb_set_goal_chooser`) rescores the satisfiable 908goals with the limit `min + margin`, keeps at most six (upstream's pick 909first, then cheapest), and hands them to the host only when there are at 910least two. `zbanks-jev` says each as a sentence, asks one Choice, samples 911the pick per the digest's `_sample_goal` (floor 0.05, T 2.0), reuses a 912remembered answer for the same option set within 1,800 frames, and on any 913failure returns upstream's pick. 914 915Measured 2026-09-21, headless, home from `states2`, map from `s1map`: 916 917| Run | Jev | Frames | Result | 918| --- | --- | --- | --- | 919| `off1` | off (hook compiled in, NULL) | 12,000 | `progress.tsv` identical, line for line, to `s2a` (built before the patch). | 920| `jev1` | on, margin 512, run limit $0.10 | 36,000 (10 game-minutes) | 31 choices, 31 questions, 0 reused, 0 fallbacks, 9 picks differing from upstream's; $0.000707 in all: **3.1 questions per game-minute, $0.000071 per game-minute**; ~150 ms a question (111-306). Still progresses: sword by 5,000, castle key guard by 11,000, Eastern Palace by 29,000, the Stalfos room's `SCRIPT_KILLALL` at 32,000. | 921 922Jev's answers mostly agree with upstream's nearest-first (median top 923probability ~0.9 on the first option), and it prefers the unvisited: at 924the castle key room it put 0.29 on "go through the south door ... to 925somewhere never visited" against 0.50 for the scripted key guard, and the 926flattened sample took the door. Log: `run/zbanks-c/jev1/jev.jsonl`. 927 928### Options Jev can tell apart (2026-09-21, evening) 929 930The window asked (frame 8,912, `run/window/jev-5.png`): "Lift the pot right 931here on this screen to see what is under it. It is about 62 steps away." and 932the same sentence with "(another one, number 2)" - 8 of the window's 73 933choices had such a pair. The user had already objected to that ("use door 934use door use door"). Options now come from more of the bot's own records 935(`zb_goal_option` in the shim: the node's centre, its screen's bounds, and 936the path `ap_goal_score` just found, read from `pgsearch.from` straight 937after each score call): which third of its screen; on Link's screen, how 938many steps east/west and north/south of him; elsewhere, the compass point, 939the exit the bot's path leaves Link's screen by and the screens it crosses; 940whether it lies on another option's path; the exit's kind from the bot's 941node name (dungeon door, entrance, screen edge, stairs, hole, ledge); the 942item lying (small key, big key). Goals that read alike on the same screen at 943a similar distance (within 8 steps or a fifth) are ONE option 944(`words::offers`); if that leaves one, nothing is asked. That pot pair, at 945the same spot in run `w2` (frame 8,805): "Lift the pot in the middle of this 946screen to see what is under it, 6 steps east and 8 steps south of Link. It 947is about 62 steps away." - one option, not asked. 948 949Input tokens per question: 534 in the window before (55 questions, from 950their cost at $0.042/MTok), 543 in `jev1`; after, 544 in `w2` (8 questions) 951and 549 in `j2` (47 questions, 501-708). Per question the longer sentences 952and the collapsed options about cancel; per choice fewer are asked (`j2`: 95373 choices, 47 asked, 18 reused, 8 not asked as one choice; 0 questions 954with two equal options). 955 956## The door `D 0x80` stall in room `$0123`: pinned against a TALK sprite (2026-09-21 night, characterized; fixed 2026-09-22, carried patch 0011) 957 958The live window's real current blocker (RESUME HERE's item 1, brain 959`jev-plays-snes.md`): `GOTO_POINT` to door `D 0x80`, screen `0x2458`, failed 960five times, ~127 frames apart, and the planner gave up. Reproduced headless 961from the window's own `resume.state` (copied to a scratch `home.state`, its 962own `map_export.txt` as `--import-map`, Jev off): the SAME position, `link 9630x5a88,0x2447` in the bot's coordinates (raw WRAM `0x0688,0x243f`), frozen 964from frame ~409 (the plan is set) through the end of a 6,000-frame run - not 965one pixel of movement, `given up: 9 waiting` (nine *different* goals burned 966through their own four attempts in that time, all funnelled through this 967same door, since a fresh headless process rebuilds every goal from the 968imported map with `attempts` reset to 0 - it does not inherit the live 969process's own exhausted goal list, so reaching true global `ap_manual_mode` 970this way would take far longer than reproducing the STALL itself). 971 972**The mechanism, not just the symptom.** `ZB_TRACE=400-450` on this same 973reproduction: the pad is `0x0400` (Down) on every single frame, and 974`link_state` stays `0x00` (standing, not even a walking animation) while 975`x,y` never change at all - the engine is refusing the move outright, not 976merely running out of time to complete it. Geometrically: sprite 4 in the 977room is `type=0xbb subtype=0x200 state=0x9 attrs=TALK|NODE`, hitbox 978`(5a78,2448) x (5a98,2470)` (`ap_snes.h`'s own per-room override making 979`0xbb`, normally a shop/vendor type, a plain `NODE|TALK` only in room 980`$123`). Link's frozen position, `(5a88,2447)`, sits one pixel NORTH of the 981hitbox's own top edge (`2447` vs `2448`) and inside its X range - pressed 982flush against it. The door's own node box, `(5a78,24c0) x (5a87,24e7)`, is 983128 pixels SOUTH of the sprite's bottom edge (`24c0` vs `2470`): to reach it 984Link must go around the sprite, not through it, and `ap_follow_targets`'s 985"Stuck Link Detected! 5a88,2447 en route to 5a78,24c0" names that far 986target directly, not an intermediate waypoint that would step him around 987it first. 988 989**Not a full physical wedge, `ZB_HUMAN` confirms - but it is not a free 990room either.** From the same reproduction, four 200-frame raw-input probes 991(`X` held throughout so the bot steps aside, `alttp.c:57`), each restarted 992fresh from the identical `home.state`: 993 994| Direction (+X) | Pad | Movement in 200 frames | 995| --- | --- | --- | 996| Up | `0x0840` | 1 px | 997| Down | `0x0440` | 1 px | 998| Left | `0x0240` | 19 px (west, toward/along the sprite's own left edge, `5a88`→`~5a75`) | 999| Right | `0x0140` | 4 px | 1000 1001South (the door's own direction) is blocked at the first pixel, matching the 1002bot's own trace; west is the only direction with any real headroom, and even 1003that stops well short of open ground - consistent with a small interior 1004(the sprite's own room is a shop-shaped space, per its `NODE|TALK` shop-type 1005sprite) where the walkable margin around the sprite is narrow rather than 1006absent. 1007 1008**Why this is not the castle-passage-door regression again, and not a quick 1009third fix in the same family.** `$BB` here genuinely IS `SPRITE_ATTR_TALK` 1010(unlike room `$0012`'s non-TALK `0x73`), so carried patch `0006`'s TALK gate 1011is not misfiring in the way it was there - this is a different failure 1012inside the CORRECT branch of that patch, not a case wrongly taking it. 1013Patch `0006`'s own `inside` exemption (`third-party/c/patches/0006-...patch`, 1014`ap_map.c`) only ever unblocks the ONE-CELL MARGIN ring immediately around a 1015sprite's raw hitbox when Link already stands in it - it shrinks the blocked 1016region from `[hb_tl, hb_br]` (hitbox + margin) to the raw hitbox alone, a 1017change of at most a few pixels at the sprite's own edge. The door is 128 1018pixels further south than that ring reaches, so the exemption cannot be 1019what is routing (or failing to route) Link there; A* found SOME 18-waypoint 1020path at all (unlike Sahasrahla's original "A* failed" case, `0006`'s reason 1021for existing), so the search can start from inside the margin - the failure 1022is in what that path asks `ap_follow_targets` to do afterward, not in 1023whether a path exists in principle. 1024 1025**Left unfixed, and why:** turning this into a general "route around a 1026solid sprite mid-path, not just avoid it as a destination or an origin" 1027fix touches the same pathfinding code that has already produced one 1028regression this session (the castle passage door, `0006`'s own `destination` 1029exemption catching an unrelated sprite) from a narrower, better-understood 1030change than this would need. Confirming a fix here would mean tracing 1031`ap_pathfind_local`'s actual grid construction for this specific room 1032against the disassembly's tile-attribute data, at the same rigor as patches 1033`0001`/`0003`/`0004`/`0006` each took - not an afternoon next to the 1034recovery mechanism this session was mainly about. Recovery itself does not 1035depend on this being fixed: retried unconditionally, this exact goal will 1036keep failing exactly this way (it is a deterministic replay from a fixed 1037position with Jev off), which is precisely what the recovery cap is for - 1038see `apps/native/src/app.rs`, `packages/zbanks/src/recovery.rs`. 1039 1040**Next step for whoever picks this up:** read `ap_pathfind_local`'s handling 1041of the cells between the sprite's margin and the door's own node box in room 1042`$123`'s tile-attribute grid (`ap_map.c`, the same table `alttp-ram-map.md` 1043documents the format of), specifically whether a WEST-then-SOUTH route 1044exists in the grid at all before touching how `ap_follow_targets` executes 1045whatever path is found - the geometry above says a route around the west 1046side is where to look first. 1047 1048### Fixed (2026-09-22): the sprite was never blocking to A* at all 1049 1050The "next step" above assumed A*'s obstacle model of sprite `$BB` was 1051correct and the bug was in how `ap_follow_targets` executed the path (or in 1052the grid's own construction). Neither was true. **Sprite `$BB`'s per-room 1053override in room `$0123` never marked it `SPRITE_ATTR_BLKF` or `BLKS` in the 1054first place** (`ap_snes.h`: `{ .type = 0xBB, .subtype = 0x0200, 1055.only_dungeon_room = 0x123, .attrs = SPRITE_ATTR_NODE | SPRITE_ATTR_TALK }`) 1056- only `NODE | TALK`. `ap_pathfind_local`'s obstacle-cost loop only marks a 1057sprite's hitbox impassable when `attrs & (SPRITE_ATTR_BLKF | SPRITE_ATTR_BLKS)` 1058(`ap_map.c`, the loop patch `0006` also touches); without either flag, this 1059sprite is invisible to that loop entirely, so A* never saw an obstacle here 1060and happily built an 18-waypoint path straight through the sprite's real 1061hitbox - a path the game's own collision then refused outright, which is 1062exactly why raw `ZB_HUMAN` input confirmed Down blocked at the very first 1063pixel while the A* grid (dumped from `local_cost.pgm`, decoded to ASCII) showed 1064no blocked cell anywhere near Link's position at all. Patch `0006`'s own 1065`destination`/`inside` exemptions were correctly scoped all along (as the 1066characterization above found) - they were simply never reached, because the 1067sprite they gate never entered the blocking code path to begin with. 1068 1069The general type entry for `0xBB` (`ap_snes.h`, above the per-room table) 1070DOES carry `SPRITE_ATTR_BLKF` alongside `SUBT` and `TALK`. Every other 1071per-room `TALK|NODE` override in the same table keeps or drops BLKF 1072correctly for what the sprite actually is: Sahasrahla's (`0x16`) keeps it (he 1073is solid); uncle's (`0x73`/`0x55`) correctly has none (he lies on the 1074ground, not solid). Room `$0123`'s override for `0xBB` is the one entry that 1075adds `NODE` (so the bot can build a TALK node for this NPC, the "Mini 1076Moldorm Cave Guy") while silently dropping the BLKF the general entry has - 1077an upstream authoring slip in a static table, not a Randomizer difference 1078and not anything a host can see around. `third-party/c/patches/0011-shopkeeper-blocks-in-room-0123.patch` 1079restores it. 1080 1081**Proof.** Reproduced headless exactly as before (the live window's own 1082`resume.state` as `home`, its `map_export.txt` imported, Jev off): before the 1083patch, frozen at `link 0x5a88,0x2447` pressing Down every frame, replanning 1084an identical 18-waypoint path every ~127-133 frames forever (`run/zbanks-c` 1085scratch, not kept). With the patch, the same start reaches the door and keeps 1086playing - by frame 600 Link has already left room `$0123` through door `D 10870x8e`; by frame 12,000 (in a longer run from the same start) he is back at 1088the shopkeeper doing `TALK_NPC` normally, no freeze (that specific NPC 1089cannot actually be satisfied - see "The shopkeeper cannot be paid" below, 1090a separate, newly-exposed limitation, not a regression from this patch). 1091 1092**Regression, a full 90,000-frame run from a freshly-made `home`** (current 1093patched ROM, `--make-home`, since the ROM's cumulative patches make any 1094older saved `home` untrustworthy - checked by cartridge-CRC refusal as the 1095safeguard), Jev off, `run/zbanks-c/s1map`'s own historical map imported (the 1096same map earlier regression proofs on this page use, and one that does not 1097yet know about the newly-reachable shopkeeper, so it does not spend time 1098proving that NPC unfixable before reaching the main objectives - see below 1099for what happens with a map that DOES already know about it): **sword by 1100frame 3,000** (`$0055`, Well Uncle), **bow by frame 42,600** (matching 1101`j10`: 43,714, and the pre-regression best), **green pendant (`$7EF374 = 11020x04`) by frame 54,600** (`j10`: ~59,000), **Sahasrahla's boots by frame 110358,200** (`j10`: 62,499) - all within the pre-regression range, `manual 1104mode: false` for the ENTIRE run (no stall of any kind, headless summary 1105confirms zero rows with `manual=true`), and still playing at the full 110690,000-frame budget with no early stop. Sahasrahla is confirmed reachable 1107(boots obtained). Nothing in the fix touches Sahasrahla's own path (patch 1108`0006`'s Sahasrahla-specific behaviour is unchanged - `0011` only adds a 1109flag to an unrelated sprite type/room pair). 1110 1111A SEPARATE run using the live window's own, much more fully-explored 1112`map_export.txt` instead of `s1map`'s reaches the newly-reachable shopkeeper 1113NPC early (its map already tags the NPC as a cheap, nearby EXPLORE-adjacent 1114option) and spends ~9,000 frames proving it unfixable (see "The shopkeeper 1115cannot be paid" below) before `apps/zbanks`'s own "stop a minute after 1116recovery gives up" behaviour ends that run around frame 34,800 - a property 1117of which map was imported and what it already believes is worth trying, not 1118evidence against the door fix itself, which is what the `s1map` run above 1119isolates. 1120 1121**Live**, restarted onto this build via MCP (`dev_mode on` -> `restart` -> 1122`dev_mode off`): the window had been pinned at this exact pixel for 1123multiple real hours (RECOVERY firing every 16,558 frames without moving 1124Link at all - see the recovery section below); after the restart it left 1125room `$0123` within the first few seconds of play. It was seen back in that 1126room later, at a nearby but different position (the shopkeeper NPC's own 1127approach point, not the original door-pin pixel) - the door-pin bug itself 1128never recurred at any point in continuous real-time observation, but the 1129window did stick again for several minutes on the SEPARATE shopkeeper 1130problem described below, surfaced only because the door fix made that NPC 1131reachable for the first time. `Recovery` was seen escalating correctly 1132(`attempt: 4` of `5` via the `bot` MCP tool) before something else on the 1133map's own goal list resolved and the bot moved on by itself, confirmed by 1134`state` showing `mode: "overworld"`, `control: true` and a real, changing 1135position across successive polls (world areas 60, 27, 52, then 24) minutes 1136later. (An MCP `press` probe used to test raw movement at the stuck spot 1137briefly and harmlessly toggled the world-map overlay open via `X` - closed 1138again the same way before the bot resumed; no directional input from that 1139probe reached the game, so the overworld movement seen afterward is the 1140bot's own.) 1141 1142## Recovery's cap never engaged: progress was scored too broadly (fixed 2026-09-22) 1143 1144Live, before the `$0123` fix above, `packages/zbanks/src/recovery.rs`'s cap 1145(5 attempts with no progress before giving up for good) never engaged 1146although the SAME stall recurred over and over: `run/native.log` shows 1147`RECOVERY attempt 1 of 5` at frames 16567, 33125, 49683, 66241, 82799, 99357 1148and 115915 - **exactly 16,558 frames apart, every single time** - never 1149`attempt 2`. Each restored the same 33 given-up goals (all of them, per the 1150window's own MCP `state`, routed through the same physically-pinned door - 1151see the section above), and Link's OWN position (`read_wram`/`state`, not 1152the task-list stack) never moved from `0x5a88,0x2447` across any of them. 1153 1154**Cause.** `Recovery::consider`'s progress signal was `possessions(wram)` 1155OR `bot.completed_count()` changing since the previous check - both GLOBAL: 1156a change anywhere on the whole known map counts, not only something that 1157would let the STUCK goal succeed. `zb_completed_count` (`packages/zbanks/shim/host.c`) 1158increments for any goal that leaves the bot's list with `attempts <= 3` - 1159which includes a goal `retry_all_given_up` just re-added from scratch 1160(attempts reset to 0) immediately re-evaluating as already satisfied against 1161the save (a chest already opened, a door already unlocked) and leaving the 1162list "complete" before Link takes a single step. With 33 goals restored 1163every cycle, at least one such trivial re-completion happened deterministically 1164within the same ~16,558-frame window each time, resetting `Schedule`'s 1165attempt counter to 0 although the one goal that actually mattered - the door 1166- kept failing for the exact same unfixed reason. 1167 1168**First fix (necessary, not sufficient on its own), `packages/zbanks/src/recovery.rs`:** 1169a completed goal now counts as progress only together with Link's own 1170position (`Place` and pixel coordinates, the same reading the caller 1171already takes for `stall::Signal`, passed into `Recovery::consider` rather 1172than re-read) having changed since the last check. A possession change 1173(`$7EF340`-range: item, key, pendant, crystal, big key, progress) still 1174counts alone - Link cannot gain one without being at its location, so a 1175global read of it is trustworthy on its own; a completed goal is not, since 1176the mechanism above can fire it without Link moving at all. 1177 1178**This alone did not fix the live incident.** A second run of the exact 1179same shape (see below - a different NPC, same room) reproduced "attempt 1" 1180forever even with this fix built in, because of a SECOND, larger bug in 1181`Schedule::poll` (the pure backoff/cap decision, deliberately kept apart 1182from `Recovery` so it is testable without a real bot/console): the 1183`!stalled` branch reset `attempts` and `next_at` to zero on ANY tick where 1184the caller reports "not stalled" - not just when real progress happened. 1185`retry_all_given_up`'s own `zb_goal_add` clears `ap_manual_mode` as a side 1186effect of adding a goal back (`c37bf1ea`, above), so `stalled` reads `false` 1187for a tick (or many) immediately after EVERY recovery attempt, whether or 1188not the goal it just revived can actually succeed. Under the old rule that 1189momentary "not stalled" reading was read as "the stall cleared, so whatever 1190was wrong is fixed" and wiped the count - meaning a stall that recurs 1191because the SAME goal keeps failing could never escalate past attempt 1, no 1192matter what `progressed` said, because the `!stalled` branch reset it first 1193on the very next tick regardless. 1194 1195**Second fix, `Schedule::poll`:** only `progressed==true` forgets the count 1196now; a bare `stalled==false` reading returns early without touching 1197`attempts`/`next_at`, so a recurrence of the same unfixed stall resumes 1198escalating from where it left off. Unit-tested (`packages/zbanks/src/recovery.rs`, 1199`buck2 test //packages/zbanks:test`, 26 tests green): a schedule fed 1200`true, false` / `false, false` / `true, false` in a loop (exactly the 1201toggle recovery's own side effect produces) escalates 1..CAP and gives up, 1202where the old code returned `Some(1)` forever. 1203 1204**Both fixes were needed, and both were proven against a SECOND live 1205incident, not just replayed in a test.** With the `$0123` door fix above 1206landed, the door stall itself cannot recur (there is no unfixed goal left 1207to keep restoring through it) - so proving the recovery fix end-to-end 1208needed a live reproduction of a DIFFERENT stall with the same shape. One 1209appeared immediately: see "The shopkeeper cannot be paid" below. Headless, 1210against that exact case (`/tmp` scratch run, not kept - reproduce with the 1211live window's own `resume.state`/`map_export.txt`, `--import-map`, Jev off): 1212`RECOVERY attempt 1 of 5` at frame 22011, then `attempt 2` at 22611, 1213`attempt 3` at 23811, `attempt 4` at 26211, and `RECOVERY gave up after 5 1214recoveries with no progress` at frame 31011 - escalating on every single 1215occurrence, matching `Schedule`'s own unit test exactly, and giving up loudly 1216rather than looping for the rest of the run. Live in the window (frame 1217counters reset by an intervening restart, so not directly comparable to the 1218headless numbers): `recovery.last.attempt` was seen at 4 of 5 via the `bot` 1219MCP tool before the underlying goal picture changed enough (see below) that 1220the stall cleared on its own. 1221 1222## The shopkeeper cannot be paid: a new, separate limitation the door fix exposed (found 2026-09-22, not fixed - out of scope for this pass) 1223 1224Fixing the `$0123` door made sprite `$BB` ("Mini Moldorm Cave Guy", 1225`NODE|TALK|BLKF`) reachable for the first time - and reachable is also 1226walkable-TO, so the bot's planner now offers a `TALK_NPC` goal for it. That 1227goal cannot succeed: headless (Jev off, the live window's own `resume.state` 1228and `map_export.txt`), the bot walked up to it, opened its text box 1229repeatedly, and the goal failed and was retried in a tight ~400-600-frame 1230cycle (`STALLED`/`RECOVERY`/`stall cleared` every cycle) until `Recovery` 1231correctly gave up for good at frame 31011 (see above). `TALK_NPC`'s own 1232gift detection (patch 0007) is "a change in `$7EF340-$7EF35F` since the talk 1233began" - the general mechanism that works for Sahasrahla and uncle. This 1234NPC's own base type (`ap_snes.h`'s general `0xBB` comment: "Salesman / 1235chestgame guy / 300 rupee giver guy / Chest game thief / Item for sale") 1236is a family of NPCs that, in vanilla, gate their gift behind a CHOICE or a 1237COST the bot has no model for at all - a "will you pay 100 rupees?" prompt, 1238a chest game, or (matching the room's own name) a "kill 4 mini-moldorms 1239first" precondition never satisfied by anything in the bot's own goal 1240system. No amount of retrying changes any of that, so `Recovery`'s cap 1241giving up on it is the CORRECT terminal behaviour, not a defect - this is 1242squarely the "no goal exists for the actual precondition" class of gap the 1243Book of Mudora section above already describes for a different NPC. 1244 1245**Once `Recovery` gave up on this one goal, in the LIVE window the bot kept 1246playing.** It has 249+ other goals on a heavily-explored map, and 1247`ap_manual_mode` (the C's own one-way latch) only ever applies to ALL goals 1248at once, not this one specifically - so once something ELSE unrelated 1249finished (a chest, an unrelated key), `zb_goal_add` cleared the latch as it 1250always does, and the bot moved on. Confirmed live: after `recovery.last` 1251showed `attempt: 4` (via the `bot` MCP tool) with the window's own 1252`stalled.json` reading "Link has not moved" for several minutes at this 1253exact NPC, `state` moments later showed `mode: "overworld"`, `control: 1254true`, and a real, changing position across successive reads (area 60, then 125527, then 52, then 24) - the bot had moved on to other goals entirely, on its 1256own, with no intervention beyond what `Recovery`'s own bounded mechanism 1257and the bot's own huge goal backlog already provide. 1258 1259**In a headless run with a MAP THAT DOES NOT YET KNOW ABOUT THIS NPC** 1260(`s1map`'s own map, from before the door fix made it reachable), the 126190,000-frame regression proof below never encounters it at all in that 1262run's own exploration order, and reaches every milestone cleanly. A run 1263seeded with a map that already tags it as a "cheap, nearby" option (the 1264live window's own heavily-explored `map_export.txt`) discovers it early and 1265spends the ~9,000 frames above proving it unfixable before moving on - cost, 1266not correctness: the same run still recovers and keeps playing afterward, 1267it is simply the fully-explored map's own EXPLORE-goal ordering that visits 1268this dead end sooner. Left unfixed on purpose, per the task that found 1269it: teaching the bot to navigate a shop's payment prompt (or add a 1270`SCRIPT_KILLALL`-style precondition script for whatever this specific room 1271actually requires) is new scope, not a regression from anything done here. 1272 1273## The shopkeeper goal declined (host), and proving the Book of Mudora (2026-09-22) 1274 1275**Confirming the requirement gate, again.** A 400,000-frame headless run 1276(`run/zbanks-c/task1-book`, `home`, `s1map`'s map, Jev off) reached boots at 1277frame 60,000 and, from then on, `goals.txt` shows the Library script 1278flipping between `unsat Needs: [BOOTS]` and `limit` (in-range, cost over the 1279search cutoff, not unreachable) depending on Link's current position and 1280search budget - the same behaviour item 4 above already found, now seen 1281continuously for tens of thousands more frames. It was never `Active goal` 1282even once. This is not new; what stopped the run from getting the chance to 1283prove it further is. 1284 1285**The shopkeeper goal (previous section) was not merely a cost - it 1286permanently derailed a long run.** At frame 91,483 the same `task1-book` run 1287walked into room `$0123`'s shopkeeper, and from there `Recovery` escalated 12881→2→3→4→5 exactly as designed and gave up for good at frame 100,797; 1289`apps/zbanks`'s own "stop a minute after recovery gives up" then ended the 1290run at 104,397 - a quarter of its budget, with the Library goal sitting the 1291whole time as a correctly satisfiable, in-range option the greedy scorer 1292never got to reach because nothing after that point ever ran again. 1293 1294**Fix: decline the goal outright (`packages/zbanks/shim/host.c`, 1295`zb_npc_cannot_be_satisfied`).** The same mechanism that already declines 1296uncle's done-in-this-save NPC goal now also declines any `GOAL_NPC` for 1297sprite `0xBB`/subtype `0x0200` in dungeon room `$0123` - the bot never 1298creates a task for it at all, so it never approaches, never opens its text 1299box, and never spends a `Recovery` attempt on it. Proven 1300(`run/zbanks-c/task1-fix`, identical start): the bot leaves room `$0123` 1301within 5,000 frames of arriving (vs. never, before) and keeps playing to at 1302least frame 190,000 - a run that used to die at 104,397 now goes 82% further 1303before hitting anything else. 1304 1305**A second bug this exposed: `apps/zbanks`'s own stop condition fired on a 1306transient stall, not just a permanent one.** `task1-fix` still hit 1307`RECOVERY gave up after 5 recoveries` at frame 186,826 (an unrelated, 1308ordinary stall - not the shopkeeper, which is gone) - and the very next 1309log line is `stall cleared at frame 186827`, through `zb_goal_add`'s own 1310"a goal was added while in manual mode; resuming the plan", which has 1311nothing to do with `Recovery`. The old rule stopped the run unconditionally 13123,600 frames after `Recovery` ever gave up, discarding the run at frame 1313190,426 while the bot had in fact been un-stalled and exploring normally the 1314entire time since 186,827. Fixed (`apps/zbanks/src/main.rs`): the 13153,600-frame grace timer now tracks "stalled AND recovery exhausted" 1316together and resets the moment either stops being true, so a transient 1317stall Recovery happened to be exhausted for no longer ends the run, while a 1318genuinely permanent one still does. 1319 1320**Proven both ways, same run (`run/zbanks-c/task1-fix2`, identical start, 1321both fixes in place):** past frame 190,000 (where the old rule would have 1322stopped it) still playing normally; `RECOVERY gave up` fires again at the 1323same frame, 186,826 (a deterministic replay), and this time the run 1324correctly continues; it finally stops for good at frame 197,331 on a 1325*different*, genuinely permanent stall (below) - `stopping at frame 197331: 1326recovery gave up and the bot has stayed stalled for a minute`, this time 1327truly warranted. Net effect of both fixes together: the run's healthy 1328lifetime went from 104,397 → 197,331 frames, roughly double, entirely by 1329removing waste, before meeting a wall that needs its own investigation. 1330 1331**The next blocker: a door at the Dam, same shape as the room `$0123` pin, 1332not yet characterized or fixed.** `ap_follow_targets` logs `Stuck Link 1333Detected! 9b08,2190 en route to 9af8,2140` repeatedly from frame 179,651 1334onward, and the task that never completes is `TRANSITION ... [node=door U 13350x80 DOOR|NODE NORM screen=Dam Chest ^ 9a00,2100 x 9bff,21ff]` - a fixed 1336pixel, a fixed unreachable target, exactly the shape carried patches 1337`0006` and `0011` each turned out to be (a real obstacle A* does not see, 1338or a route that requires going around one). Not investigated this session: 1339whoever picks this up should start the same way those were - a `ZB_TRACE` 1340bracket on the approach to this door and a dump of `ap_pathfind_local`'s 1341grid for this room, the same method that found `0006`'s regression and 1342`0011`'s missing `BLKF`. 1343 1344**The live window separately needed two operational recoveries this 1345session, neither a code bug:** 1346 13471. `Recovery`'s cap (`packages/zbanks/src/recovery.rs`) is a one-way latch 1348 for the life of the process by design - once spent, it never retries 1349 again, whatever the reason it was spent on. The window hit this 1350 legitimately (the shopkeeper, pre-fix) and sat permanently stalled with 1351 nothing able to clear it. Only a `restart` (dev mode on → `restart` → 1352 off) gives it a fresh `Recovery`/`Bot`; this is expected maintenance, not 1353 a defect, and is now the second time this exact remedy has been needed. 13542. After a `load_state("home")` reset, the window's own auto-imported map 1355 (`run/zbanks-window/map_export.txt`, copied to `map.19.txt` on every 1356 `restart` per `apps/native/src/app.rs`) still reflected a much 1357 later-game topology than Link's new position, and a fresh Bot built from 1358 it found every reachable goal `unsatisfiable` (no known route) within 1359 ~600 frames, then genuinely exhausted `Recovery`'s cap with 0 goals ever 1360 restored (nothing to retry - the graph was just disconnected from here). 1361 Renaming both `map.19.txt` and `map_export.txt` aside (kept, not 1362 deleted, as `*.bak-2026-09-22-0105`) before the next `restart` + 1363 `load_state("home")` let the bot build a fresh, internally-consistent 1364 graph from its actual position, and it got the sword and shield within 1365 1,500 frames and stayed healthy (no stall, `recovery.gave_up: false`) 1366 for a confirmed 10+ minutes of continuous real-time play afterward. 1367 1368## The Dam Chest push block: a hammer peg that was really a wall (fixed 2026-09-22, carried patch 0012) 1369 1370The next blocker after the shopkeeper fix (previous section): `ap_follow_targets` 1371logged `Stuck Link Detected! 9b08,2190 en route to 9af8,2140` repeatedly, and the 1372live window's own user, watching the window, gave the ground truth a trace alone 1373would not have: at the Dam Chest room, Link stands beside the grey push blocks and 1374repeatedly swings the **hammer** at one, gets nowhere, and eventually wanders off 1375to an unrelated goal - the same shape as the shopkeeper, but with a different tool 1376mistaken for the right one. 1377 1378**Reproduced fast and cheaply**, without touching the live window: the live 1379window's own `resume.state` and `map_export.txt`, copied (not read live - the 1380window was later closed) into a scratch states directory and used as `home` for 1381`apps/zbanks`, Jev off. A fresh process rebuilds its goal list from the imported 1382map with `attempts` reset to 0, so it re-picks the same door goal from scratch in 1383under 10,000 frames rather than needing the live process's own already-mostly- 1384exhausted state - the same trick used for every stall reproduced this way in this 1385document. 1386 1387**The mechanism, read directly out of the running C** (a temporary `LOG` line in 1388`ap_follow_targets`, added, proved, and reverted - never committed, per this 1389file's own "Proving a patch that changes pathing" note): at the exact stuck 1390position, `next_xy = target.tl = (0x9af8, 0x2178)`, one grid cell north of Link - 1391an ordinary, close waypoint, not a stale far-away target. `ap_map_attr(next_xy)` 1392there is raw tile `0x27`, and `ap_tile_attrs[0x27] = TILE_ATTR_HMMR` - `ap_snes.h`'s 1393own comment on that entry already flags the ambiguity: `// hammer peg (was also 1394fence; remapped to 0x01)`. Dungeon room `$010B` (the Dam's block-puzzle room) 1395reuses the same tile id for its push blocks, a THIRD meaning the same comment 1396never anticipated. With `TILE_ATTR_HMMR` set and the hammer owned, 1397`ap_follow_targets`'s branch order (`ap_map.c`) presses Y at it every other frame 1398(`JOYPAD_MASH(Y, 0x1)` - the trace's alternating `0x4800`/`0x0800` pad values, 1399step 2 apart, are exactly this) - forever, since a push block does not respond to 1400a hammer swing. 1401 1402**Not merely a wrong tool - a wrong PASSABILITY judgment too.** `ap_pathfind_local` 1403computes its own `lift_mask` *including* `TILE_ATTR_HMMR` whenever the hammer is 1404held, and scores an HMMR tile `cost = 20` (a normal, low-cost, "interact and pass" 1405tile) rather than the `1 << 20` a wall gets - so A* built an 18-ish-waypoint path 1406straight onto this tile in the first place, the same shape as patches `0003` and 1407`0011`. Confirmed live: `push_timer` ($7E0371) was actively counting down frame by 1408frame throughout the stall (`0x1b` → `0x19` → `0x17` → `0x13` over ~150 frames) 1409while Link's own x,y never moved a single pixel - the real engine's push mechanic 1410genuinely engaging (UP is held unconditionally whenever the target is north, 1411regardless of any tile-attribute branch), consistent with the block already having 1412been shoved as far as it can go and Link now pushing against something immovable, 1413matching what the user watched happen: "he pushes the first block up, and it is 1414now sitting between him and the chest." 1415 1416**Fix (`0012-dam-push-block-not-a-hammer-peg.patch`, `ap_map.c`).** A new 1417`ap_tile_attrs_here(raw_tile)` returns `0` for tile `0x27` specifically when 1418`*ap_ram.dungeon_room == 0x010B`, and `ap_tile_attrs[raw_tile]` unchanged 1419everywhere else - wired into both of `ap_follow_targets`'s attribute checks and 1420`ap_pathfind_local`'s per-tile costing loop. With attrs forced to 0 in this one 1421room, the tile is no longer "liftable" (no more A-mash) or `TILE_ATTR_HMMR` (no 1422more Y-mash), and `ap_pathfind_local`'s costing falls through to its own `else` 1423branch - `cost = (1 << 20)`, a real wall - so A* treats it exactly like any other 1424solid obstacle it cannot cross. No host fix: it is the exact same 1425static-table-lacks-room-context bug as `0003`/`0011`, just for a tile id instead 1426of a sprite type. 1427 1428**Proof.** Reproduced headless from the (now-closed) live window's own 1429`resume.state`/`map_export.txt`, Jev off: unpatched, `given_up` climbs to 1 within 1430~9,600 frames (the door goal fails its three attempts and is abandoned) and the 1431task list shows the identical `Stuck Link Detected!`/hammer-mash pattern seen 1432live. Patched, the SAME reproduction (`run/zbanks-c/live6`): `given_up` stays `0` 1433for the entire 20,000-frame run - the bot leaves the Dam Chest room cleanly via a 1434different door (`door D 0x8e`) within the same few thousand frames, and the door 1435`U 0x80` goal itself now scores `limit` (in range, over the search-cost cutoff - 1436the same harmless, ordinary "book of Mudora" shape) rather than ever being 1437attempted and failing. No `Stuck Link Detected` anywhere in the patched run's log. 1438 1439## The hammer swings at bare floor (fixed 2026-09-22, carried patch 0030, movement/combat item 1) 1440 1441The user, watching the live window: at frame 88684, in Sahasrahla's hut, the 1442active task was `LIFT_POT [node=pot 0x71 NODE|LFT0]` and the pad showed 1443Y+Right with the hammer equipped - Link swung the hammer at bare floor 1444instead of walking to the pot and pressing A. `0012` (previous section) 1445had already fixed the identical shape once, but only for dungeon room 1446`$010B`: `ap_tile_attrs_here` returned `0` for raw tile `0x27` in that one 1447room and left every other room's `0x27` going through the unmodified flat 1448`ap_tile_attrs[0x27] = TILE_ATTR_HMMR`. 1449 1450**Why raw `0x27` is never a real hammer peg indoors, from the disassembly.** 1451zelda3's `HandleItemTileAction_Dungeon` (`src/dungeon.c:81dabb`) is the real 1452game's own version of "is the tile in front of Link interactable, and how": 1453it only ever recognizes an interactive dungeon object through the 1454"replacement object" family - raw BG attribute `0x70-0x7F`, whose low 1455nibble indexes `dung_replacement_tile_state[]` (`$7E0500`, 16 `uint16_t` 1456slots). `RoomDraw_HammerPegSingle` (`dungeon.c:81b493`) writes `0x4040` 1457into a slot for a real peg; a pot's slot is `0x1010` 1458(`kDungeon_QueryIfTileLiftable_rv`, matching this brain's own 1459`alttp-ram-map.md`, "Liftables, named and gated" - dungeon liftables/pegs 1460are `$70-$7F`, translated back through this same table); a plain pushable 1461block (`DrawObjects_PushableBlock`, `dungeon.c:81b4d6`) writes `0`, neither 1462pattern. Raw `0x27` is a completely different attribute value, outside 1463this family - it is `TileBehavior_Hookshottables` in the OUTDOOR 1464tile-detection switch (`src/tile_detect.c:342`), and `ap_snes.h`'s own 1465comment on its `0x27` entry ("was also fence") already shows it is reused 1466for unrelated static scenery per-tileset. Indoors, `ap_map_attr_from_ram` 1467reads straight from the game's own live `dngn_bg1_tattr`/`dngn_bg2_tattr` 1468buffers (`$7F3000`/`$7F2000`, matching `dung_bg1_attr_table`/ 1469`dung_bg2_attr_table` in zelda3's `variables.h`) - so raw `0x27` indoors is 1470whatever that tileset's designer used the id for: a push block's floor in 1471`$010B`, apparently a plain floor tile in Sahasrahla's hut. Nothing in the 1472real game's own hammer-peg check ever consults it. 1473 1474**Fix (`0030-hammer-peg-indoors-general.patch`, `ap_map.c`), no room 1475number anywhere.** `ap_tile_attrs_here` now strips `TILE_ATTR_HMMR` 1476whenever `*ap_ram.in_building` is set, unconditionally, before returning 1477the flat table's value - `ap_snes.h`'s table sets that bit for exactly one 1478raw id (`0x27`), so this is equivalent to "indoors, `0x27` is never treated 1479as a hammer peg," full stop. For dungeon room `$010B` specifically this 1480produces the identical result `0012` did (`0x27`'s only bit is HMMR, so 1481stripping it zeroes the same way `0012`'s hardcoded `return 0` did), so 1482`0012`'s own fix is now subsumed rather than duplicated - both patches stay 1483carried (in application order `0012` then `0030`) since `0030` composes on 1484top of `0012`'s introduction of `ap_tile_attrs_here` rather than 1485replacing it. Outdoors is untouched: `ap_map_attr_from_ram`'s existing 1486fence carve-out (`tile != 0x021B`) already covers the one known outdoor 1487ambiguity, and real outdoor hammer pegs keep working exactly as before. 1488 1489**Proof.** A fresh 100,000-frame headless run (`--make-home`, canonical 1490open-mode home, Jev off, `--no-retry`) reaches the same healthy shape prior 1491runs in this document show before hitting manual mode: `PICKUP 16/25, 1492CHEST 13/20, EXPLORE 99/155, ITEM 0/0, NPC 0/0, SCRIPT 3/3`. Only two 1493`Stuck Link Detected` events in the whole run, both at the same cell in 1494Blind's Basement (`ab10,23b0` en route to `ab10,23a0`) - a pre-existing, 1495unrelated jam (`ap_jam_note` fired for it, the general `0015` mechanism 1496already handling it), not a hammer mash. No regression: the run never held 1497the hammer at all in this playthrough (too early), so the fix's own branch 1498(`*ap_ram.inventory_hammer != 0` still gates the Y-press) never even 1499engaged, and the general A*/pot-lift pathing this patch touches is 1500unaffected by it either way. The exact historical frame (88684, Sahasrahla's 1501hut) was not re-reproduced pixel-for-pixel - the live window that produced 1502it is closed and its save was not kept - but the fix is a direct, 1503disassembly-grounded removal of the only code path that could have pressed 1504Y there, and is provably behaviour-preserving for the one case (`$010B`) 1505that was previously proven live. 1506 1507## The Eastern Palace big chest: no big-key gate at all (fixed 2026-09-22, carried patch 0013) 1508 1509While the Dam fix was being proven, the live window's own user reported a SECOND 1510failing goal, and that the bot was oscillating between it and the Dam: at Eastern 1511Palace's big chest, "Eh? It's locked! If you had the Big Key…" - repeatedly. 1512 1513**Root cause, read from the code, not guessed.** `ap_goal_score`'s `GOAL_CHEST` 1514case (`ap_plan.c`) only ever checked `sram_room_state` for "already opened"; 1515nothing checks whether the chest CAN be opened. `ap_node_islocked` (`ap_map.c`) 1516does have a correct, working big-key check - `DOOR_ATTR_BKEY`, `*ap_ram.sram_ 1517dungeon_bigkeys & (1 << (15 - dungeon_id))` - but that branch only ever fires 1518for `node->type == NODE_TRANSITION` (a locked DOOR). Its own `NODE_CHEST` branch, 1519a few lines further down, asks only "is this chest tile still closed" (`lock_attr 1520== 0x27` there means "opened chest" - yet another, unrelated meaning for the same 1521raw tile id 0x27 the Dam fix above deals with, in a different context/plane - 1522confirming these ids really are heavily overloaded throughout this codebase) and 1523never consults `sram_dungeon_bigkeys` at all. So a big chest was always 1524"reachable" to the pathfinder and always score-eligible to the goal system, 1525however keyless the run, and the bot walked up and got told no every single time. 1526 1527**This one failing goal was not isolated - it fed a ping-pong.** With ~200,000 1528frames of exploration behind it and 60 goals already given-up-and-restored, the 1529window's own `Recovery` kept retrying every given-up goal on a stall 1530(`zb_retry_given_up`, `packages/zbanks`), the big chest included; each retry 1531walked most of the map to reach it, failed identically, and the resulting stall 1532retried everything given-up again - including whatever OTHER far-away goal (the 1533Dam door, before its own fix landed) was also failing, so the two traded places 1534indefinitely, burning real wall-clock time on a network of unfixable and 1535soon-to-be-fixed goals rather than ever reaching new content. 1536 1537**Fix (`0013-big-chest-needs-the-big-key.patch`, `ap_plan.c`).** `GOAL_CHEST`'s 1538own scoring now also checks, when `goal->node->chest_type == 1` (upstream's own 1539ROM-derived "this is THE big chest" flag, set from the dungeon's chest table - 1540`ap_map.c`, `chest_id & 0x8000`): `*ap_ram.sram_dungeon_bigkeys` for 1541`goal->node->screen->dungeon_id`'s bit, returning `GOAL_SCORE_UNSATISFIABLE` 1542if it is not set - the exact bit formula `ap_node_islocked`'s own `NODE_KEYBLOCK` 1543branch already uses, and deliberately the per-NODE `screen->dungeon_id` rather 1544than the global "current dungeon" `ap_req`'s generic requirement system would 1545have used (that system's own `REQUIREMENT_KEY` already carries a `// XXX how to 1546handle this across dungeons` admission of the same imprecision - this fix does 1547not repeat it, since a chest's own dungeon is always known statically from its 1548node, not from wherever Link currently stands). 1549 1550**Why this alone stops the ping-pong, without a separate retry-suppression 1551mechanism.** `GOAL_SCORE_UNSATISFIABLE` goals are skipped in `ap_goal_evaluate`'s 1552own scoring loop before a goal is ever chosen `min_goal` - they never become the 1553active goal, never generate a task, never physically fail, and therefore never 1554reach `ap_goal_fail`/the given-up list `zb_retry_given_up` walks. An unsatisfiable 1555big chest just sits in `ap_goal_list` forever, silently, exactly like the Book of 1556Mudora's own `unsat`/`limit` goal before boots - the SAME already-working 1557mechanism that makes a correctly-gated goal harmless, now covering a chest for 1558the first time. Combined with the Dam fix (which stops that goal from ever 1559failing either), neither side of the reported ping-pong can occur any more: no 1560new mechanism was needed once both root causes were actually fixed. 1561 1562## Never push a block into a wall (general rule, fixed 2026-09-22, carried patch 0015) 1563 1564The Dam Chest fix (`0012`, previous section) closed one instance of a shape that 1565recurs by construction, not by bad luck: `ap_snes.h`'s own comment on raw tile 1566`0x27` already lists three unrelated meanings for the same id (fence, hammer peg, 1567push block), and `0012`'s fix only teaches the host the THIRD one, and only in 1568dungeon room `$010B`. Every other room that reuses `0x27` (or any other tile id) 1569for a push block the static `ap_tile_attrs[]` table cannot see room-context for 1570will produce the identical stall - Link stands next to an already-jammed block 1571mashing the wrong tool forever - and would need its own hand-found room check, 1572forever, exactly the pattern `0011`/`0012`/`0014` were already three instances of 1573before `0014` itself got generalized (previous sections). 1574 1575**Why no static table generalizes this.** The SNES's own tile system reuses 1576numeric ids per room BY DESIGN (each room selects a graphics/tileset bank that 1577reinterprets the same indices) - there is no bit anywhere in a raw tile id that 1578says "this one is a push block here." A lookup table keyed by id, however large, 1579is the wrong shape for a per-room reinterpretation; every future occurrence would 1580be undiscovered until a user watched the window stall and reported it by hand. 1581 1582**The general signal is not the tile at all - it is what the GAME does.** 1583`push_timer` ($7E0371) engages whenever Link genuinely starts a push/pull/lift 1584interaction, real regardless of room or tile id (confirmed directly for the Dam 1585case in `0012`'s own section: `push_timer` counted down frame by frame while 1586Link's x,y did not move a single pixel). `ap_follow_targets` already runs a 1587generic stuck detector (`stationary_link_count >= 128`, pre-existing, unrelated 1588to any of this) that fires today for stalls of every kind and merely resets the 1589target timeout, letting `ap_pathfind_local` immediately re-select the identical 1590blocked path next time. The fix teaches that existing, already-generic detector 1591to REMEMBER a jam instead of only forgetting the timeout. 1592 1593**Fix (`0015-never-push-a-block-into-a-wall.patch`, `ap_map.c`).** A small 1594per-screen memo - `ap_jam_screen`/`ap_jam_cells[8]`/`ap_jam_count`, all file-scope 1595statics, no change to `struct ap_screen` itself. `ap_jam_sync(screen)` clears the 1596memo the instant the active screen differs from the one it was last touched for - 1597correct, not merely conservative, because this codebase's own understanding of 1598ALTTP (`Room-reset-before-fail`, next section) already establishes that a room's 1599pushed-block state resets ONLY on a real screen transition, so "the active screen 1600changed" is exactly the event that must invalidate previously-learned jams, no 1601more and no less. `ap_follow_targets`'s stuck-detector calls `ap_jam_note(screen, 1602target)` when it fires AND `*ap_ram.push_timer != 0` (an interaction was actually 1603engaged, as against every OTHER reason `ap_follow_targets` can get stuck, which 1604this patch leaves untouched). `ap_pathfind_local`'s per-cell costing loop 1605consults `ap_jam_is(screen, mapxy)` as its FIRST branch, ahead of even the 1606destination-bbox and `0x1C` special cases, forcing `cost = (1 << 20)` (an 1607ordinary wall) - a cell proven immovable cannot be reclassified as free just 1608because a goal's own bbox happens to cover it. 1609 1610**Proof, organic, not synthetic.** `0012`'s own fix was disabled for one 1611diagnostic build only (its room/tile guard changed from `dungeon_room == 0x010B` 1612to an unreachable `0xFFFF`, `git checkout --` after - the same temporary-and- 1613reverted method this file's own "Proving a patch that changes pathing" note 1614already establishes), isolating `0015` as the only defense against the ORIGINAL 1615bug. A fresh `--make-home` boot, no map, Jev off (`run/zbanks-c/jam-proof/ 1616isolation`) reached the Dam at frame 3000 on its own and reproduced the exact 1617historical position from `0012`'s own section (`9af8,2140`, one grid cell from 1618the documented `9af8,2178`/`9af8,2140` pair) at frame 3408: 1619 1620``` 1621[3408 ap_map.c:ap_follow_targets] Stuck Link Detected! 9af8,2180 en route to 9af8,2140 1622[3408 ap_map.c:ap_jam_note] Jammed cell noted: 9af8,2140 in screen Dam Chest ... - treating it as a wall until the room resets 1623[3408 ap_plan.c:ap_goal_fail] Failed goal: EXPLORE [node=door U 0x80 ... screen=Dam Chest ...] 1624``` 1625De-duplication held (the same cell was not re-logged on the goal's two further 1626retries at frames 3535/3662), and the goal was correctly declared unsatisfiable 1627(`Permafailing goal`, frame 3789) once no path around the now-known wall existed 1628- the right call with `0012` disabled, since the underlying misclassification 1629that would let Link approach correctly is still gone in this diagnostic build. 1630Leaving and later re-entering the SAME room (frame ~10129, a fresh transition) 1631re-triggered `ap_jam_note` for the SAME cell independently - direct proof the 1632per-screen memo is cleared and correctly re-learned across a real room reset, 1633not merely never cleared. 1634 1635With the real `0012` restored (`git checkout --` undid the diagnostic edit, 1636verified byte-for-byte against the pre-edit copy) and `0015` still present, the 1637same fresh-home reproduction crosses the Dam room on both of its two visits 1638(frames 3000-4800 and 5400-9600) with zero "Stuck Link Detected"/"Jammed cell 1639noted" lines - `0012`'s preventive, room-scoped fix still does its job on the 1640first attempt, and the general mechanism stays fully dormant, proving the two 1641are compatible rather than one masking the other. Separately, the Eastern 1642Palace door-offset reproduction (`run/zbanks-c/jam-proof/ep-repro`, `0014` + 1643`0015` together, 20,000 frames from the same fixture used to prove `0014`) 1644finished with `given_up` at `0` for the entire run and zero jam events - no 1645regression from adding the general mechanism alongside the door-offset one. 1646 1647## A one-time NPC's reward already held is never offered again (fixed 2026-09-22) 1648 1649Live, right after a restart rebuilt the goal list from nothing (frame 3806): 1650the bot walked straight back to Sahasrahla's hut and talked to him again, 1651although Link already held the Pegasus Boots. Unlike a chest (`GOAL_CHEST` 1652reads `sram_room_state`), a talk-for-item NPC's goal had no such re-check at 1653all - once created, it stayed offerable forever regardless of the save. 1654 1655**Where, and why not in the C.** `packages/zbanks/CLAUDE.md`'s own rule: 1656"never edit the bot" - a behaviour the host needs is a change in 1657`shim/host.c`, not a carried patch, unless no host can make it from outside. 1658This one can: `ap_map.c`'s `ap_goal_add` calls are compiled to go through 1659the host's `zb_goal_add` (`-Dap_goal_add=zb_goal_add`, 1660`third-party/c/BUCK`), and that function already had exactly this shape - 1661`zb_done_in_this_save` declines uncle's own goal once his SpritePrep bit 1662fires, to avoid a stale-memory assert. The Sahasrahla case is the same 1663INTERCEPT POINT, a different REASON (a wasted trip, not a crash). 1664 1665**Fix, table-driven.** A new `zb_npc_reward_already_held` in 1666`shim/host.c`, checked in `zb_goal_add` alongside the existing two declines: 1667one row per known one-time-reward NPC (`ap_snes.h`'s room-scoped 1668`ap_sprite_attrs_for_type` table has exactly three `TALK|NODE` overrides - 1669Sahasrahla, uncle, and room `$0123`'s shopkeeper, which is handled 1670separately since its own problem is "can never succeed", not "already 1671succeeded"). Sahasrahla ($16/$0000): Pegasus Boots, `$7EF355` 1672(`inventory_base` `$7EF33F` + `BOOTS`'s own index `0x16` - cross-checked 1673against `research/alttp-ram-map.md`'s own citation, zelda3 1674`variables.h:1085`, "link_item_boots"). Uncle ($73/$0100): the sword, 1675`$7EF359` - a second, independent check alongside 1676`zb_done_in_this_save`'s own crash-avoidance one, since the sword can be 1677held before uncle's own SpritePrep bit is set. The next one-time NPC is a 1678row, not a new function. 1679 1680**Proof.** Headless from the live window's own `resume.state` (boots 1681already held) with its current map imported, 3,000 frames: frame 0 logs 1682`zbanks host: not adding goal for sprite 0x16.0 BLKF|TALK|NODE: reward 1683already held (boots already held)` and Sahasrahla's goal never appears 1684again for the rest of the run - the bot spends the whole run on unrelated 1685EXPLORE/transition tasks instead. 1686 1687**Not yet built: the NPC completion check is goal-CREATION-time only**, 1688matching the existing `zb_done_in_this_save` pattern exactly (both check at 1689`zb_goal_add`, not via a re-scored `ap_goal_score` branch, since scoring has 1690no host-replaceable hook the way goal creation does). This is sufficient for 1691the reported bug (a restart rebuilds the whole goal list from nothing, so 1692the check runs fresh for every node), but an NPC goal created BEFORE this 1693fix existed, still sitting in a long-lived process's goal list, would not 1694retroactively decline until that process restarts. 1695 1696## A routine already done, or already claimed, is never offered again (fixed 2026-09-22, carried patch 0016) 1697 1698Two related complaints from the user, watching the live window. 1699 1700**Hyrule Castle.** Jev picked "Carry out the known routine 'HC kill guard for 1701key'" at 70% - Link walked back to the castle, went in, turned around and 1702left. `ap_scripts[]` (`ap_map.c`) has two live Hyrule Castle routines, "HC 1703kill guard for key" and "HC kill miniboss free Zelda" (`SCRIPT_KILLDROPS`, no 1704`start_item`, no requirement) - both exist only to progress the vanilla 1705rescue sequence: kill a guard for its key, then the miniboss guarding 1706Zelda's cell. `zbanks::rando::PRESET` (`packages/zbanks/src/lib.rs`) sets 1707`$7EF3C6` (`sram_progress2`) to `0x14` from file creation - bit `0x10` 1708(`uncle_left_house`) and bit `0x04` (`zelda_at_sanctuary`) both already set, 1709matching `packages/alttp`'s own `Progress::read` (what the `state` tool's 1710`"progress":"zelda_rescued"` reflects). Both routines' entire purpose is 1711already accomplished before the bot's first frame, in every game this 1712project starts - not conditionally, structurally, since open mode IS this 1713preset. 1714 1715**Blind's House.** Separately, Jev picked "Blind's House Block Puzzle" at 171679% (frame 41857) with the room's own chests already open - the routine had 1717nothing left to solve either, a different cause (a claimed reward, not a 1718skipped storyline beat) with the same symptom. 1719 1720**Fix (`0016-hc-routines-already-done-in-open-mode.patch`, `ap_plan.c`), two 1721checks in `ap_goal_score`'s `GOAL_SCRIPT` case.** (1) `GOAL_SCORE_UNSATISFIABLE` 1722for any script node whose name starts `"HC "` once `*ap_ram.sram_progress2 & 17230x04` is set - the same "read the live flag, gate the goal" shape as `0010`'s 1724boots requirement and `0013`'s big-key gate. The two commented-out HC entries 1725share the prefix, so re-enabling either inherits the gate for free. (2) A 1726GENERAL check, for every routine rather than only the castle's: walk the 1727script's own screen's node list: if it has at least one `NODE_CHEST` and 1728every one already shows its `sram_room_state` bit set (the identical read 1729`GOAL_CHEST`'s own scoring already does), the routine is `GOAL_SCORE_COMPLETE` 1730- nothing left to solve. A routine whose screen has no chest at all (AgTower's 1731statues/curtain, the Kak well jump) is untouched, since `has_chest` stays 1732false. 1733 1734**Proof.** The HC gate: a fresh headless run from `home` with the live 1735window's own current map imported (`run/zbanks-c/hc-gate/final`, since a 1736truly fresh, mapless run had not discovered Hyrule Castle within 15,000 1737frames tried) shows, from the first evaluation after the script node is 1738registered, `unsat` for the whole run: 1739``` 1740attached node Script: HC kill guard for key 1741* unsat SCRIPT [node=Script: HC kill guard for key screen=HC First Key ^ 5200,e00 x 53ff,eff] on HC First Key ^ 5200,e00 x 53ff,eff Needs: [Any] 1742``` 1743The chest-completion check is code-reviewed, not yet live-fire proven: it 1744reuses `GOAL_CHEST`'s own already-proven bit formula verbatim over a node 1745traversal, but no fixture on disk has Dam's or Blind's chest ALREADY open at 1746the moment its script goal is (re-)scored - in every headless repro tried, 1747the script's own task-completion fires (wrongly - see below) before the 1748chest opens, so the new branch is never exercised end to end. It becomes 1749testable once the next fix lands. 1750 1751## A finished routine that did not reach its goal is a failure, not a success 1752 1753The Dam Chest script itself is the reason the check above could not be 1754live-fire proven: `ap_goal_complete` fired for "Dam Chest Block Puzzle" at 1755frame 2528, BEFORE the chest opened at frame 2591 - the goal is marked 1756complete when its `SCRIPT_SEQUENCE` task finishes running its hardcoded 1757movement string, not when the thing the script exists to do (open the 1758chest) actually happens. A script whose steps are wrong for this cartridge 1759(the user's own "D" report: two blocks pushed beside the chest, one below, 1760chest still shut, frame 26507) still reports success and the bot wanders off 1761to unrelated exploration - the exact "worthless, looks-fine failure" this 1762section exists to name. Fix and general Dam Chest push-block solver: see 1763this file's own upcoming section (this session's item D). 1764 1765## Jev is not told a routine already failed here (open, item E) 1766 1767A third complaint from the user watching live: at frame 23806, Jev's options 1768were the Dam Chest routine (already failed on earlier visits) against an 1769unexplored overworld edge, with nothing in the request telling Jev the Dam 1770option was a repeat of a known failure - `state`/the goal-choice `Facts` 1771carried no attempt history at all, so Jev could not weigh "try again" against 1772"explore something new". Fix: a per-goal outcome log kept by the host (the 1773C side already has `goal->attempts`; the host sees every fail/retry) surfaced 1774in `Facts`, and narrowing when `zb_retry_given_up`/`Recovery` put a failed 1775goal back to only when something that bears on ITS OWN failure changed - a 1776requirement now held, the room reset, or its routine fixed - rather than any 1777possession change at all. Not yet built; this section is the record of why. 1778 1779## Never ask Jev to break a tie the scorer already settles (fixed 2026-09-22, item B) 1780 1781At frame 14674 (and, in the log, frame 8739's own instance of the identical 1782shape) Jev's options were "Lift the pot ... 27 steps away" against "Lift the 1783pot ... 45 steps away" - two pots in the same room, differing only in 1784distance. `goal_choice/words.rs`'s `offers()` already collapsed same-kind, 1785same-place goals into one offer, but only when their distances were 1786`similar` (within 8 steps or a fifth of the longer) - a threshold meant to 1787stop two sentences reading IDENTICALLY at a glance, not to decide whether 1788Jev should be asked. The planner's own scoring already weighs distance, so 1789asking Jev to pick the nearer of two otherwise-identical goals spends a 1790question and teaches nothing, at any distance gap. 1791 1792**Fix.** `offers()`'s merge condition drops `similar(g.steps, steps)` 1793entirely - same description and same screen now always collapse to one 1794offer, however far apart the distances are. `similar()` itself is deleted 1795(its only caller). The group's representative (`goals[0]`, the goal actually 1796pursued if the offer is picked) is kept as the NEAREST of the merged set as 1797each candidate streams in, rather than whichever happened to be seen first - 1798"not asked: same kind, nearest taken" is the resulting behaviour, not just 1799the log line's wording. 1800 1801**Proof.** `packages/decisions/src/goal_choice/words.rs`'s own test 1802`same_place_far_apart_in_distance_is_not_collapsed` (asserting 2 offers for 1803a 200-vs-900-distance pair) is rewritten as 1804`same_place_far_apart_in_distance_is_still_collapsed_and_the_nearer_is_taken` 1805(asserting 1 offer, with the nearer goal - index 1, the 200-distance one - 1806as the representative even though it was listed second). Full 1807`packages/decisions:test` suite green (20/20): the three tests that rely on 1808DIFFERENT descriptions staying apart (`pots_in_different_places_are_told_apart`, 1809`pots_close_together_are_placed_by_steps`, `sentences_say_what_where_and_the_way`) 1810are unaffected, since they never shared a description to begin with. 1811 1812**How many of the logged questions this would have skipped.** `jev.jsonl` 1813only records each option's RENDERED SENTENCE, not the original 1814`GoalOption` structs `offers()` groups on, so this is a text-level 1815reconstruction (strip each option's trailing distance clause and count 1816groups that collapse to one), not a byte-exact re-run of the real function - 1817stated as what it is, not overclaimed. Of 776 lines in 1818`run/zbanks-window/jev.jsonl` where `how` shows a real `asked` question (not 1819reused, one-choice, throttled or a fallback), **20 (2.6%) would have been 1820skipped entirely** under the new rule - every one is exactly this shape (two 1821or three same-kind, same-room options differing only by a distance clause): 1822frame 92 (two pots, 2 vs 9 steps), frame 3388 (two pots, 19 vs 23), frame 18238739 (two pots, 27 vs 45 - the cited example, one frame count off from how 1824it was first reported live), frame 23504 (three chests, 2/7/9), frame 23577 1825(two chests, 6 vs 8), and 15 more of the same shape. 1826 1827## Room-reset-before-fail: designed, not built 1828 1829Separately asked for, generically: when a goal fails inside a room whose state 1830the bot changed (a pushed block, and so on), retry by first leaving through the 1831nearest door and re-entering (which resets ALTTP's own per-room puzzle state) for 1832up to `K` tries, before counting it as a real failure against the goal's attempt 1833limit and `Recovery`'s cap. Not built this session, for a specific, structural 1834reason rather than lack of time to spare: this project's only outward hooks after 1835a goal is chosen are (a) `ap_goal_choose_hook` (patch `0002`), which is offered 1836only the goals `ap_goal_evaluate`'s OWN scoring loop has already decided are 1837`GOAL_SCORE_COMPLETE`-eligible or better - and a door already known to lead 1838somewhere mapped scores `GOAL_SCORE_COMPLETE` for a plain `EXPLORE` goal without 1839ever generating a walk-there task, so the obvious "make the chooser pick the 1840nearest door goal twice" does not actually make Link walk anywhere; and 1841(b) `ap_goal_fail`, which is `static` to `ap_plan.c` and has no host hook at all 1842yet (unlike `ap_goal_add`, it cannot be redirected with a `-D` rename trick from 1843another file, since every caller is inside the same translation unit). 1844 1845A real implementation needs, at minimum: a new `ap_goal_fail_hook` carried patch 1846(mirroring `0002` exactly - a NULL-by-default function pointer called from 1847`ap_goal_fail` before its own `attempts++`/permafail logic, proven a no-op the 1848same way `0002` was, by a byte-identical run with and without it), plus a genuine 1849host-driven "walk to the nearest door and back" maneuver that does NOT go through 1850`ap_goal_add`/`ap_goal_score` at all - since as established above, a known door 1851cannot be forced to generate a task through the ordinary goal system. The 1852nearest-existing analogue is `ap_target_scripted`/`ap_scripts[]` (`SCRIPT_SEQUENCE`, 1853patches `0009`/`0010`), which drives Link through a fixed waypoint list outside 1854goal-scoring - but those are hand-authored per room at compile time, not 1855something a host can construct at runtime for an arbitrary room's arbitrary 1856nearest door. That gap - a host-constructible, non-scored "go here, then there" 1857task - is the piece a future session needs to add before this feature can exist; 1858everything else (the per-node failure-count bookkeeping, the K-retries-then-real- 1859failure threshold) is ordinary host-side (Rust) work once that primitive exists. 1860Not attempted half-built: a partial version that LOOKS like it resets the room 1861without actually forcing the walk would be worse than not having it, per this 1862project's own standard for what counts as done. 1863 1864## A door with no vestibule ($0089), and the resume stall it caused (2026-09-22) 1865 1866A window resumed mid-game (`resume`, indoors room `$0089`) stalled in manual 1867mode from frame 0, forever: `bot` (MCP) showed exactly 2 `EXPLORE` goals, both 1868scoring `unsatisfiable`, both targeting the room's own two north doors 1869(`door U 0x4b`). This was first suspected to be the already-known stale 1870imported map bug (`map.19.txt` disconnected from Link's real position, 18712026-09-21), and renaming the map aside was tried live - it changed the 1872symptom (`goal_count` dropped from 232 to 2, an honestly-scanned room instead 1873of a stale one) but not the stall, proving the two are different bugs. 1874 1875**Reproduced headless, deterministically**, by `save_state`-ing the live 1876stalled window and using it as a fresh `home` for `apps/zbanks` 1877(`run/zbanks-c/ep-repro/`, `--states` pointed at a directory holding only that 1878one snapshot as `home.state`): the same two doors, same `unsatisfiable`, same 1879manual mode from frame 0, every time. 1880 1881**Ruled out, with evidence, in order:** 1882 18831. *Missing cross-screen adjacency (a sibling multi-cell quadrant never 1884 linked).* A temporary, never-committed instrumentation pass on 1885 `ap_pathfind_global`/`ap_pathfind_node` (printf per call, gated on 1886 nothing so it always fires) showed `start_screen == destination->screen` 1887 (`same=1`) for both goals - the destination is on LINK'S OWN screen. The 1888 Dijkstra cross-screen graph is never even reached; `ap_pathfind_local` 1889 fails first. Any static same-`dungeon_room` pre-link (mirroring the 1890 stairs/ledge merges) would be a no-op for this bug. 18912. *A genuine local-pathfinding misclassification (a real tile wrongly 1892 scored as a wall).* A second instrumentation pass dumped 1893 `ap_pathfind_local`'s own cost grid (`raw_tile`/cost per cell) around 1894 both Link's position and the registered destination, and a flood-fill 1895 over the parsed grid confirmed the destination box is disconnected from 1896 every cell Link's own position can reach - matching the search's own 1897 verdict. But raw `ZB_HUMAN` movement (no bot in the loop) proved this 1898 ISN'T a real wall: holding LEFT/RIGHT alone walks Link's `x` to exactly 1899 each door's own `tl.x`; holding DOWN from north of the door crosses its 1900 tile and lands in a DIFFERENT ROOM (`$00a9`) within 20 frames. The grid 1901 is disconnected because the registered DESTINATION - not the path to 1902 it - is unreachable: it sits 24px past the door, on the far side of an 1903 instant, no-vestibule transition. 19043. *A mis-detected door direction.* Every door's `adjacent_direction` is 1905 guessed from which edge of its own bookkeeping screen it sits nearest 1906 (`ap_map.c`'s `d_t`/`d_l`/`d_b`/`d_r` comparison), then asserted (not 1907 corrected) against the ROM's own door-direction table 1908 (`ap_ram.dungeon_door_dirs`) - a mismatch is a silently-continued 1909 `assert_bp` (a SIGTRAP), never fixed up. Suspected here, since a wrong 1910 guess would explain both the offset and a failed `TASK_TRANSITION`. 1911 Disproven: making the fix "trust the ROM's own direction when it 1912 disagrees" produced ZERO mismatches for this door (traps: 0) - the 1913 heuristic's "U" already agrees with the ROM. The theory that `$00a9` is 1914 simply this same room's lower floor (`$0089 + 0x20`, `XYFLIPBG`'s own 1915 x match at `0x88b0`) was also checked against the real landing position 1916 and rejected: Link's `y` after the transition (`~0x147b`) is roughly 1917 1024px past where a same-quadrant floor-below would land (`~0x1068`) - 1918 this is a real, separate, distant room, not an adjacent floor. 1919 1920**Root cause and fix: every door's registered approach point is offset 24px 1921past its own tile, in its own direction, assuming a few pixels of floor 1922stand between "in front of the door" and the transition** - correct for 1923every other door this bot has ever crossed (Eastern Palace's own clear run 1924depends on it), wrong for this one, which has no vestibule at all: touching 1925its tile transitions instantly. The 24px offset therefore places the goal on 1926the far side of a line Link's own room can never reach by walking, and 1927`ap_pathfind_local` correctly, permanently, reports it unsatisfiable. 1928Patch `0014` (`ap_map.c`, room `$0089`, `tile_attr 0x4b` only): use a 0px 1929offset instead - the door's own tile, which `ap_pathfind_local` already 1930treats as passable within 3 grid-cells of Link's start (the `XYL1DIST(...) 1931<= 3` exemption every EXPLORE-a-door goal already relies on to be reachable 1932at all once Link is close), with no assumption about which side has 1933clearance. 1934 1935**Proven, headless, from the exact reproduced stall (`run/zbanks-c/ep-repro/`, 193620,000 frames, deterministic across repeated runs):** both goals go from 1937permanently `unsatisfiable` to active and pursued within the first few 1938hundred frames (`Plan: EXPLORE via GOTO_POINT`), the bot walks through the 1939door and the room changes - `$0089` (start) → `$00a8`/`$00a9` (by frame 19402,000-3,000) → `$00aa` → `$00b9` → `$00c9` → `$0105` → `$0112` → `$010b` 1941("Dam Chest", by frame 20,000) - recognizably Eastern Palace's own room 1942names appearing in the goal list (`EP Catwalk`, `EP Dodgeball`, `EP 5 pots`, 1943`EP Chest&Ledge`) along the way. `manual mode: false` at the end: the bot is 1944genuinely playing, not idling in a shorter recoverable cycle. 1945 1946## Outstanding, as of 2026-09-22 (queue after the movement/combat split) 1947 1948Recorded so the queue survives a session boundary. The coordinator split 1949this engagement's queue: a SEPARATE agent now owns movement and combat 1950(hammer-on-floor / 0012 generalized, dash as a path action, glove gating, 1951dash-when-faster, stopping a dash, melee/bow/vulnerability, projectiles - 1952its patches number from 0030) - none of that is this file's concern from 1953here. This agent kept the goal-system/planning half, patches numbered 19540016-0029. 1955 1956**Done, committed:** item 7 (bot.log real rotation via a pipe and a 1957dedicated thread, `1ddd5b0d`); item A (routines with nothing left to do - 1958the HC gate and the general chest-open check, patch `0016`, `2daf5bf4`); 1959patch `0015` (general push-block jam detection, `d34f01b1`). 1960 1961**Queued, in the order last given:** 19621. **B - Jev asked a worthless question**: frame 14674, two pots differing 1963 only in distance (27 vs 45 steps). Only ask Jev when offers differ in 1964 KIND (screen/area, goal type, item/requirement) - collapse same-kind 1965 options in the same room to "not asked: same kind, nearest taken", 1966 extending the existing equivalence merge in `goal_choice/words.rs`. 1967 Report how many of the 728 questions in `run/zbanks-window/jev.jsonl` 1968 the new rule would have skipped. 19692. **F - argmax vs. sample threshold**: frame 68593 sampled a 10% option 1970 against a clear favourite (wasted trip); frame 77784 sampled a 19% 1971 option. Decode rule: top probability >= 0.70 takes the argmax; below 1972 that, keep sampling (close calls still explore). Record which rule fired 1973 ("took favourite" / "sampled, close call") in the Record and show it in 1974 the panel badge. Unit-test both branches. NOTE: `packages/jev-http` 1975 (`ledger.rs`/`lib.rs`/`README.md`) showed uncommitted changes mid-session, 1976 not made by this agent - check `git log`/`git status` before starting; 1977 another agent may already be on this. 19783. **E - goal outcome history + narrower retry**: Jev's options at frame 1979 23806 didn't say the Dam routine had already failed here. Keep a per-goal 1980 outcome log in the host (C already has `goal->attempts`; host sees every 1981 fail/retry) and surface it in `Facts` (attempts, how each ended, frames 1982 since last try). Narrow `zb_retry_given_up`/`Recovery`'s put-back rule: 1983 only when something bearing on THIS goal's own failure changed (a 1984 requirement now held, the room reset, its routine fixed) - not on any 1985 possession change. Prove headless: no return trip to a failed goal 1986 without a relevant change. 19874. **D - general push-block puzzle solver** (do not hand-fix the Dam 1988 script): BFS over block configurations from the room's live tile 1989 attributes and block sprite positions - state is (Link region, block 1990 positions), a move is a legal push (target tile floor, not wall/another 1991 block, reusing 0015's knowledge) - search until the goal tile (chest/door 1992 approach) is reachable, then execute via existing push/GOTO machinery. 1993 Use for every push-block room; Dam Chest's routine becomes a caller. 1994 Prove headless on Dam Chest (chest's SRAM bit set) and one other 1995 push-block room. Also folds in the already-identified bug: a 1996 `SCRIPT_SEQUENCE` task finishing must not count as its goal being reached 1997 - the Dam script finished at frame 2528 with the chest still shut and the 1998 goal wrongly marked complete; a finished routine that did not reach its 1999 goal must count as a FAILURE (see this file's own section on this, 2000 above). NOTE: generalizing 0012 itself (removing its `dungeon_room == 2001 0x010B` room check) moved to the movement/combat agent's list - this 2002 agent's own general push-block MECHANISM (0015) already stands 2003 independently of whether 0012 stays room-scoped. 20045. **Item 2** - `failure_recovery`, the `ap_goal_fail` hook, and a room-exit 2005 task, proven on Dam Chest `0x010B`. 20066. **The frame-57040 stall** - see "Paused mid-investigation" below. 20077. **Fresh-game sword with no map, plus a new canonical baseline** - see 2008 this file's own bisect finding (next paragraph): make a fresh game 2009 without a map actually get the sword, then rebuild the 90k baseline from 2010 `home` and record its milestone frames here (`s1map` is gone). 20118. **The generalized-0014 verdict** - land or drop the graduated door-offset 2012 generalization (`run/saved-patches/0014-generalized-graduated-offset.patch`) 2013 against the new baseline from item 7, and say which. 20149. **Item 5** - the sourced, web-researched location table for 2015 `goal_choice::Facts`. 2016 2017**Item 7's own finding (already recorded, still load-bearing for item 7 2018above):** the "fresh run from `home` never gets the sword" symptom is NOT a 2019code regression - a headless run at `1e493ef2` itself, with the coordinator's 2020own exact bisect protocol (fresh `--make-home`, no map, Jev off), also never 2021gets the sword out to 30,000 frames. The historical "sword ~3,000 frames" 2022figure required the missing `s1map` import giving the bot pre-existing 2023location knowledge; blind EXPLORE alone does not converge on it at any 2024commit tested. No commit needs bisecting; what is missing is either a 2025rebuilt map fixture (tracked in git this time, not gitignored `run/`, so it 2026cannot go missing again) or a real "seed goals from the current screen on a 2027mapless start" mechanism - not yet designed. 2028 2029**Paused mid-investigation, evidence preserved:** the frame-57040 EP 2030Bigchest stall (`second_stall_frame57040` save state, `roms/*.states/`). 2031Findings so far, from headless repro (`run/zbanks-c/frame57040/`) with 2032temporary, reverted debug instrumentation: (1) `ap_update_map_screen(false)` 2033resolved "current screen" as `0x04b8` while Link's own Y position 2034(`0x0538`=1336) falls inside the ADJACENT screen `0x05b8`'s range 2035(1280-1535) - an off-by-one-quadrant misresolution, seen specifically with 2036`link_lower_level` set (a multi-level room) - this alone makes 2037`GOAL_PICKUP`'s `goal->node->screen != screen` check wrongly reject pots 2038Link stands next to. (2) Even when screen resolution IS correct (observed 2039once, frame 157), `ap_pathfind_local` still returns -1 for a pot on that 2040SAME, correctly-matched screen - a genuine local A* bug independent of (1). 2041(3) The GLOBAL Dijkstra fallback ALSO fails for every goal on that screen 2042(search exhausts with no route). Root cause not yet isolated to a single 2043fix; likely candidates given `link_lower_level`'s involvement: the room's 2044BG1/BG2 layer selection or Y-origin computation for the lower level of a 2045multi-quadrant room. Next step: read `ap_update_map_screen` 2046(`ap_map.c:2363`) and the `link_lower_level` branch (`ap_map.c:273`) 2047against how the mismatched screens' own bounds were registered.