For agents, on top of README.md, which they read first.
How to make one, and the rules every patch obeys, are in ../CLAUDE.md. A
new patch gets a row in README.md's table and an entry below, in the same
commit as the patch.
The carried patches, in full
0001-follow-targets-on-intra-room-stairs.patch(alttp.c) - bcb2537's early return for$B0 != 0also caught the intra-room stairs, which count their own steps in$B0; the bot never consumed the targets it climbed past and walked back down. Upstream's video (commit 7add0e6) predates that line. Evidence:../../../research/zbanks-alttp.md.0002-goal-choice-hook.patch(ap_plan.c) -ap_goal_choose_hook, a function pointerap_goal_evaluatecalls with upstream's own pick (the lowest score), that score andap_goal_score, and whose return it pursues. NULL by default, which is upstream's behaviour exactly (measured: a 12,000-frame run identical, line for line, with and without it). It is a patch and not a shim wrapper because the choice is inside astaticfunction between two statements; the shim (../../../packages/zbanks/shim/host.c,zb_set_goal_chooser) installs it.0003-anti-fairy-circle.patch(ap_snes.h, ap_map.c, ap_plan.c) - Eastern Palace$B8, where the big key is: the switch that brings out its chest is under a pot an anti-fairy circle (sprite$82, HP 255) orbits until the room's other enemies are dead. Upstream calls the circle a plain enemy, so paths ran through it and Link was pinned by knockback for 19,000 frames (runs2a). Four parts:$82is invulnerable and blocking, and the anti-fairies it breaks into ($15) invulnerable (the game does not count them for a clear room either); kill-all skips invulnerable sprites; aSCRIPT_KILLALLentry "EP clear big key room" in upstream's own per-room script table, as it has for the castle's key guard; and kill-all no longer fails its goal the first frame an enemy has no path to it (tries the others, waits 8 frames, gives up after 32 misses) - three such frames permafailed the script in runb4. No host fix: the room is the same in vanilla and the Randomizer, and the only thing a host could change is the room itself (delete the enemies), which is changing the game, not hosting it. Upstream never needed$B8(its seed's big key was elsewhere; its video never enters it). Evidence:../../../research/zbanks-alttp.md, "Eastern Palace$B8".0004-bow-shot-survives-follow-targets.patch(ap_map.c) - bcb2537 (upstream's last commit, 2020-02-06) added anelse { JOYPAD_CLEAR(A); JOYPAD_CLEAR(B); JOYPAD_CLEAR(Y); }toap_follow_targets.ap_tickzeroes the pad every frame, so the only Y that line can clear is one the caller set just before -SCRIPT_KILLALL's bow shot at a VBOW sprite (ap_plan.c) - and it cancelled that shot on the frame it was pressed. Upstream killed the Armos Knights at 8f13023 (2020-01-02), before the line existed. The patch drops only the Y. Measured fromj9's last frame, Jev off: unpatched (a0) 12,000 frames, no arrow, five knights at 48 HP; patched (a1) all dead and the pendant held up by frame 3,500. No host fix: the host never sees the Y, which is set and cleared inside oneap_tick. Evidence:../../../research/zbanks-alttp.md, "The Armos Knights".0005-green-pendant-is-bit-4.patch(ap_req.c) -REQUIREMENT_GREEN_PENDANTtested$7EF374 & 0x01, the red pendant. The green one (Courage) is0x04: vanilla Sahasrahla tests it (usdasm bank_05.asm:20560-20562), so does the Randomizer (z3randomizer dialog.asm:355, sram.asm:139 "- - - - - g b r"). The Eastern Palace gives green, so Sahasrahla's NPC goal (the only user of the requirement, ap_plan.c:135) stayed unsatisfiable and the boots never came (runo2, 83,580 frames, manual mode). No host fix: the bot reads the byte itself.0006-walk-up-to-a-blocking-npc.patch(ap_map.c, ap_plan.c) - a talking NPC that isBLKF(Sahasrahla,0x16.0) could not be reached:ap_pathfind_localmakes a blocking sprite's hitbox plus one 8-pixel cell impassable, a sprite node's box is the sprite's own box, and that margin rings the box ("A* failed, min heuristic: 2", runo3at 59,353, him standing there). Three parts, each needed in turn (runso3c): the sprite being walked to does not block the search for it; Link, whomXYLINKINcan never fit inside a solid NPC's box, has arrived at a talking sprite once he stops within 8 pixels of it (TALK_NPCthen presses up and A, as upstream wrote it); and a sprite whose margin Link already stands in blocks only its own cells, or every search from there fails and every goal is unsatisfiable. Upstream's own earlier answer, a TALK node placed below the sprite, is still there commented out (072b195). Both exemptions are gated onSPRITE_ATTR_TALK. The first version compared a blocking sprite's hitbox against the CURRENT pathfind's destination box with no check that the sprite was the thing being walked to, so any blocking sprite near an unrelated destination went transparent to the search while staying solid to the engine - room$0012's own0x73sprite did exactly this to the Hyrule Castle secret passage's door, freezing every fresh run there. Evidence: "The castle passage door".0007-talk-to-an-npc-until-it-gives.patch(ap_plan.c) -TALK_NPCtook any nonzero$02D8as the NPC's gift, but$02D8keeps the last item Link was given (the stale value behind runo1's abort at uncle); now only a new, nonzero one counts (the game writes 0 as the NPC starts giving). While the NPC's text box is up the task does not time out (Sahasrahla's text outlasts 64 frames). And when the talk is over Link steps down off the NPC for 20 frames: talking pressed up into it, and the next path started with a sidestep along its solid body that went nowhere.0008-text-box-gets-a-in-any-state.patch(alttp.c) -ap_tickplans, and so mashes dialog (ap_task_evaluate), only while Link is on the ground. After the boots, the A closing Sahasrahla's text starts a dash ($5D = 0x11,kPlayerState_StartDashin zelda3 player.h:21 - the bot's enum calls0x11FALLING_LEDGE), and the box stayed open for good. Now an open text box (module$0E, not the menu) gets A in any state.0009-pegasus-boots-dash.patch(ap_map.c) -SCRIPT_SEQUENCEgains four characters (8/2/4/6, numpad directions), each a target that holds A WITH its direction for one 16px step; repeat one to cover the ~29-frame charge and the travel. Upstream never wrote a dash anywhere in its history.ap_follow_targets's per-tile clear of A is skipped only when the current target's mask holds A together with a direction, so every existing script is untouched. Built, proven to compile and to replay the early game unchanged.0010-book-of-mudora-goal.patch(ap_map.c, ap_plan.c) - anap_scripts[]entry for the Library's shelf (room$0107, entrance$49), its approach point derived from ROM data (the entrance table, the sprite's own room-relative bytes cross-checked against two others in the same room) rather than a live walk-in, andREQUIREMENT_BOOTSadded inap_goal_addby the script's name ("Library Book of Mudora", the same pattern as Sahasrahla's pendant check). Proven headless to attach to the right screen and to gate correctly (unsatwithout boots,limit/in range with them); NOT yet proven to land the item. Evidence: "The Book of Mudora".0011-shopkeeper-blocks-in-room-0123.patch(ap_snes.h) - the per-room override for sprite type0xBB/subtype0x0200in dungeon room$0123droppedSPRITE_ATTR_BLKFwhile addingNODE, unlike every otherTALK|NODEoverride (Sahasrahla keeps BLKF; uncle correctly has none - he is lying down). Without BLKF the pathfinder never saw this sprite as an obstacle, so A* routed straight through its real hitbox and the engine's own collision refused it - Link pinned one pixel from the sprite, pressing Down forever at doorD 0x80("Stuck Link Detected! 5a88,2447 en route to 5a78,24c0"). No host fix: it is upstream's own static table. Evidence: "The door D 0x80 stall in room $0123".0012-dam-push-block-not-a-hammer-peg.patch(ap_map.c) - raw tile0x27isTILE_ATTR_HMMR(a hammer peg) inap_snes.h's flat, room-agnosticap_tile_attrs[]- upstream's own comment there already flags the ambiguity ("was also fence; remapped to 0x01"). Dungeon room$010B(the Dam's block-puzzle room) reuses the same tile id for its grey push blocks, soap_pathfind_localscored it "liftable" (cost 20, not a wall) andap_follow_targetsswung a hammer at it - Link stood there mashing Y forever, one pixel from a solid block, while ordinary directional walking against it still ran the game's own push mechanic partway (confirmed live:push_timercounting down with Link's own x,y frozen). Fix: a newap_tile_attrs_here(raw_tile)helper returns 0 for tile0x27specifically in room$010B, leaving every other room's0x27untouched; wired into both call sites. No host fix: it is the same static-table-lacks-room-context class as0011. Evidence: "The Dam Chest push block".0013-big-chest-needs-the-big-key.patch(ap_plan.c) -ap_goal_score'sGOAL_CHESTcase checked only whether the chest was already opened, never whether it could legally be opened:ap_node_islocked's ownDOOR_ATTR_BKEYbig-key check (ap_map.c) only ever fires for locked DOORS, neverNODE_CHEST. The bot walked straight up to a keyless big chest (Eastern Palace's) every time and got "Eh? It's locked! If you had the Big Key…" - a wasted, repeating attempt that fed a ping-pong with an unrelated failing goal across the whole map. Fix: whengoal->node->chest_type == 1(upstream's own ROM-derived "is this THE big chest" flag), the goal isGOAL_SCORE_UNSATISFIABLEuntil*ap_ram.sram_dungeon_bigkeyshas that dungeon's bit - the same bit formulaap_node_islocked'sNODE_KEYBLOCKbranch already uses (node->screen->dungeon_id, not the global "current dungeon", so it stays correct for a chest in a dungeon Link is not currently in). An unsatisfiable goal is silently skipped byap_goal_evaluateforever - it never becomes active, never fails, and never enters the given-up/ retry cycle, which is what actually stops the ping-pong (not a retry-suppression mechanism layered on top). Evidence: "The Eastern Palace big chest".0014-door-with-no-vestibule-in-room-0089.patch(ap_map.c) - every door's approach point is offset 24px past its own tile, assuming a little floor stands between "in front of it" and the transition. Room$0089's own north door (tile_attr 0x4b) has none: crossing its tile is instant, so the generic offset places the goal on the far side of a line Link's own room can never reach -unsatisfiableforever, the resume-time stall this was built to fix. Fix: 0px offset for this one door only (room$0089,tile_attr 0x4b), landing on the door's own tile, whichap_pathfind_localalready treats as passable within 3 grid-cells of Link's start - the same exemption every other door's EXPLORE goal already depends on to be reachable at all. Two other theories (a missing cross-screen adjacency link; a mis-detected door direction) were checked with headless instrumentation and evidence and ruled out before this one. Proven: a 20,000-frame headless run from the exact reproduced stall walks through the door and keeps playing across seven more rooms. Evidence: "A door with no vestibule ($0089)".0015-never-push-a-block-into-a-wall.patch(ap_map.c) -0012's fix is room-scoped (dungeon_room == 0x010B), and every OTHER room that reuses a tile id for a push block the staticap_tile_attrs[]cannot see room context for will stall the identical way until someone finds it by hand - the same shape0014's generalization closed for door offsets. No static table generalizes this: the SNES's own per-room tile reinterpretation means a raw id alone never says "push block here." The general signal is dynamic instead -push_timer($7E0371) engaging while Link's position does not move, observed by the pre-existing generic stuck detector (stationary_link_count >= 128inap_follow_targets). Fix: a small per-screen memo (ap_jam_screen/ap_jam_cells[8]/ap_jam_count, file statics, cleared the instant the active screen changes - correct, not conservative, because a room's pushed-block state resets only on a real screen transition). The stuck detector callsap_jam_notewhen it fires withpush_timer != 0;ap_pathfind_local's costing loop consultsap_jam_isas its very first branch, ahead of the destination-bbox and0x1Ccases, forcing a wall cost. Proven organically:0012disabled for one diagnostic build (dungeon_room == 0x010B→0xFFFF, reverted after), a fresh--make-homerun reproduced the exact historical Dam stall at frame 3408 and0015alone caught it, correctly re-learned it on a second room visit at frame ~10129, and stayed silent for the whole run once the real0012was restored alongside it. Evidence: "Never push a block into a wall".0016-hc-routines-already-done-in-open-mode.patch(ap_plan.c) - two checks inap_goal_score'sGOAL_SCRIPTcase. Every"HC "-prefixed script (killing a castle guard/miniboss) exists only to progress the vanilla rescue sequence, which the open-mode preset (zbanks::rando::PRESET) marks done from frame 0 (sram_progress2 & 0x04) - gatedunsatisfiableon that bit, by name prefix so a currently-commented-out HC script inherits it too. Separately, a GENERAL check for every routine: if its own screen has aNODE_CHESTand every one already shows itssram_room_statebit (the identical readGOAL_CHEST's own scoring uses), the routine isGOAL_SCORE_COMPLETE- Jev was offered "Blind's House Block Puzzle" at 79% with that room's chests already open. A screen with no chest (AgTower, Kak well) is untouched. The HC gate is proven live-fire (unsatfrom the first scoring pass); the chest check is code-reviewed only (reusesGOAL_CHEST's already-proven formula) since every fixture on disk completes the script's task before its chest opens - see the next entry. Evidence: "A routine already done, or already claimed, is never offered again".- A
SCRIPT_SEQUENCEtask finishing is not the same as its goal being reached, and treating them as the same is a live bug, not yet fixed: "Dam Chest Block Puzzle" isap_goal_completed the instant its hardcoded movement string ends, whether or not the chest the script exists to open actually opened. The wrong push sequence for this cartridge (seeresearch/zbanks-alttp.md's Dam Chest section) still reports success and the bot wanders off - looks-fine failure, the exact shapeunrepresentable-over-documentedwarns about. Do not "fix" this by special-casing Dam; the general rule is that aGOAL_SCRIPT's completion must be judged by the SAME state check its own scoring uses (the chest check above, or an equivalent), not by the task queue draining. 0030-hammer-peg-indoors-general.patch(ap_map.c, jev item 1) - generalizes0012: at frame 88684 in Sahasrahla's hut, the pad showed Y+Right with the hammer equipped while the active task wasLIFT_POT- Link swung the hammer at bare floor instead of walking to the pot.ap_tile_attrs_here(introduced by0012, room-scoped to$010B) still letap_follow_targetsmash Y at any OTHER room's reuse of raw tile0x27(ap_snes.h's onlyTILE_ATTR_HMMRentry). The real game never treats0x27as a hammer peg indoors at all: zelda3'sHandleItemTileAction_Dungeon(dungeon.c:81dabb) only ever recognizes a peg through the "replacement object" family (raw attribute0x70-0x7F, a slot index intodung_replacement_tile_state[], written0x4040byRoomDraw_HammerPegSingle) -0x27indoors is always a plain solid tile reused per-tileset (a push block's floor in$010B, this hut's floor here). Fix:ap_tile_attrs_herenow stripsTILE_ATTR_HMMRwhenever*ap_ram.in_buildingis set, unconditionally - no room number anywhere, and for$010Bspecifically this is exactly equivalent to0012'sreturn 0(raw0x27's only bit is HMMR, so stripping it zeroes the same way). Outdoors is untouched: there, raw0x27(TileBehavior_Hookshottables) genuinely is a hammer/hookshot peg, andap_map_attr_from_ram's own fence carve-out already covers that ambiguity. Proven: a fresh 100,000-frame headless regression (canonical--make-home, Jev off) reaches the same healthy shape as prior runs (PICKUP 16/25, CHEST 13/20, EXPLORE 99/155, only 2 stuck-link events, neither hammer-related) with no new jams or regressions. No host fix: the flat table's raw-id reuse is the SNES's own per-tileset reinterpretation, the same class0011/0012/0015already established a host cannot see from outside. Evidence: "The hammer swings at bare floor".0031-dash-as-path-action.patch(ap_snes.h, ap_map.c) - addsinventory_boots($7EF355) to the bot's RAM table, foldsTILE_ATTR_BONK(raw tile0x57, bonk rocks, which the game breaks only whilelink_is_running) intoap_pathfind_local's lift mask once the boots are held (so the existing corner logic forbids a diagonal approach for free), and gives the one waypoint that steps onto a BONK tile 0009's A-held-with-direction bits, soap_follow_targetsdrives the charge and the dash with no new per-frame logic. Found at Lost Woods0x0a06, frame 92877: Link had the boots but only walked Up into the obstacle. The patch cites a research section, "Dash as a path action", thatresearch/zbanks-alttp.mddoes not have yet.0032-glove-gating-general.patch(ap_plan.c) - the glove requirement on a liftable's goal comes from its tile's ownTILE_ATTR_LFT1/LFT2bits ($52/$55Power Glove,$53/$56Titan's Mitt;alttp-ram-map.md, "Liftables, named and gated"), notnode->tile_attr == 0x55, which only ever gated the big grey rock.REQUIREMENT_GLOVES_2existed inap_req.hwith no caller. The patch cites a research section, "Glove gating", thatresearch/zbanks-alttp.mddoes not have yet.- Proving a patch that changes pathing: every run diverges from the first
frame, so the A/B that isolates one place is a replay with the patch
gated on
ap_frame(--save-atto find the frame) - a throwaway build, never committed. See../../../research/zbanks-alttp.md, "The boots".