jevsnes.git / third-party / c / patches

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 != 0 also 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 pointer ap_goal_evaluate calls with upstream's own pick (the lowest score), that score and ap_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 a static function 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 (run s2a). Four parts: $82 is 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; a SCRIPT_KILLALL entry "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 run b4. 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 an else { JOYPAD_CLEAR(A); JOYPAD_CLEAR(B); JOYPAD_CLEAR(Y); } to ap_follow_targets. ap_tick zeroes 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 from j9'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 one ap_tick. Evidence: ../../../research/zbanks-alttp.md, "The Armos Knights".
  • 0005-green-pendant-is-bit-4.patch (ap_req.c) - REQUIREMENT_GREEN_PENDANT tested $7EF374 & 0x01, the red pendant. The green one (Courage) is 0x04: 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 (run o2, 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 is BLKF (Sahasrahla, 0x16.0) could not be reached: ap_pathfind_local makes 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", run o3 at 59,353, him standing there). Three parts, each needed in turn (runs o3c): the sprite being walked to does not block the search for it; Link, whom XYLINKIN can never fit inside a solid NPC's box, has arrived at a talking sprite once he stops within 8 pixels of it (TALK_NPC then 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 on SPRITE_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 own 0x73 sprite 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_NPC took any nonzero $02D8 as the NPC's gift, but $02D8 keeps the last item Link was given (the stale value behind run o1'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_tick plans, 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_StartDash in zelda3 player.h:21 - the bot's enum calls 0x11 FALLING_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_SEQUENCE gains 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) - an ap_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, and REQUIREMENT_BOOTS added in ap_goal_add by 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 (unsat without 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 type 0xBB/subtype 0x0200 in dungeon room $0123 dropped SPRITE_ATTR_BLKF while adding NODE, unlike every other TALK|NODE override (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 door D 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 tile 0x27 is TILE_ATTR_HMMR (a hammer peg) in ap_snes.h's flat, room-agnostic ap_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, so ap_pathfind_local scored it "liftable" (cost 20, not a wall) and ap_follow_targets swung 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_timer counting down with Link's own x,y frozen). Fix: a new ap_tile_attrs_here(raw_tile) helper returns 0 for tile 0x27 specifically in room $010B, leaving every other room's 0x27 untouched; wired into both call sites. No host fix: it is the same static-table-lacks-room-context class as 0011. Evidence: "The Dam Chest push block".
  • 0013-big-chest-needs-the-big-key.patch (ap_plan.c) - ap_goal_score's GOAL_CHEST case checked only whether the chest was already opened, never whether it could legally be opened: ap_node_islocked's own DOOR_ATTR_BKEY big-key check (ap_map.c) only ever fires for locked DOORS, never NODE_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: when goal->node->chest_type == 1 (upstream's own ROM-derived "is this THE big chest" flag), the goal is GOAL_SCORE_UNSATISFIABLE until *ap_ram.sram_dungeon_bigkeys has that dungeon's bit - the same bit formula ap_node_islocked's NODE_KEYBLOCK branch 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 by ap_goal_evaluate forever - 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 - unsatisfiable forever, 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, which ap_pathfind_local already 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 static ap_tile_attrs[] cannot see room context for will stall the identical way until someone finds it by hand - the same shape 0014'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 >= 128 in ap_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 calls ap_jam_note when it fires with push_timer != 0; ap_pathfind_local's costing loop consults ap_jam_is as its very first branch, ahead of the destination-bbox and 0x1C cases, forcing a wall cost. Proven organically: 0012 disabled for one diagnostic build (dungeon_room == 0x010B → 0xFFFF, reverted after), a fresh --make-home run reproduced the exact historical Dam stall at frame 3408 and 0015 alone caught it, correctly re-learned it on a second room visit at frame ~10129, and stayed silent for the whole run once the real 0012 was 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 in ap_goal_score's GOAL_SCRIPT case. 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) - gated unsatisfiable on 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 a NODE_CHEST and every one already shows its sram_room_state bit (the identical read GOAL_CHEST's own scoring uses), the routine is GOAL_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 (unsat from the first scoring pass); the chest check is code-reviewed only (reuses GOAL_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_SEQUENCE task 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" is ap_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 (see research/zbanks-alttp.md's Dam Chest section) still reports success and the bot wanders off - looks-fine failure, the exact shape unrepresentable-over-documented warns about. Do not "fix" this by special-casing Dam; the general rule is that a GOAL_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) - generalizes 0012: at frame 88684 in Sahasrahla's hut, the pad showed Y+Right with the hammer equipped while the active task was LIFT_POT - Link swung the hammer at bare floor instead of walking to the pot. ap_tile_attrs_here (introduced by 0012, room-scoped to $010B) still let ap_follow_targets mash Y at any OTHER room's reuse of raw tile 0x27 (ap_snes.h's only TILE_ATTR_HMMR entry). The real game never treats 0x27 as a hammer peg indoors at all: zelda3's HandleItemTileAction_Dungeon (dungeon.c:81dabb) only ever recognizes a peg through the "replacement object" family (raw attribute 0x70-0x7F, a slot index into dung_replacement_tile_state[], written 0x4040 by RoomDraw_HammerPegSingle) - 0x27 indoors 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_here now strips TILE_ATTR_HMMR whenever *ap_ram.in_building is set, unconditionally - no room number anywhere, and for $010B specifically this is exactly equivalent to 0012's return 0 (raw 0x27's only bit is HMMR, so stripping it zeroes the same way). Outdoors is untouched: there, raw 0x27 (TileBehavior_Hookshottables) genuinely is a hammer/hookshot peg, and ap_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 class 0011/0012/0015 already 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) - adds inventory_boots ($7EF355) to the bot's RAM table, folds TILE_ATTR_BONK (raw tile 0x57, bonk rocks, which the game breaks only while link_is_running) into ap_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, so ap_follow_targets drives the charge and the dash with no new per-frame logic. Found at Lost Woods 0x0a06, frame 92877: Link had the boots but only walked Up into the obstacle. The patch cites a research section, "Dash as a path action", that research/zbanks-alttp.md does not have yet.
  • 0032-glove-gating-general.patch (ap_plan.c) - the glove requirement on a liftable's goal comes from its tile's own TILE_ATTR_LFT1/LFT2 bits ($52/$55 Power Glove, $53/$56 Titan's Mitt; alttp-ram-map.md, "Liftables, named and gated"), not node->tile_attr == 0x55, which only ever gated the big grey rock. REQUIREMENT_GLOVES_2 existed in ap_req.h with no caller. The patch cites a research section, "Glove gating", that research/zbanks-alttp.md does 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-at to find the frame) - a throwaway build, never committed. See ../../../research/zbanks-alttp.md, "The boots".