host.c690 lines · 28.6 KB · raw
1// The host side of zbanks/alttp that is easier to say in C than in Rust.
2// It reads the bot's own state for the panels, gives `assert_bp` the debugger
3// it was written for, and - only when the host asks - makes the goal choice
4// and puts back goals the bot gave up on. The bot's own code is not changed.
5//
6// Upstream: ../../../third-party/c/zbanks-alttp (zbanks/alttp bcb2537).
7
8#define _GNU_SOURCE
9#include <signal.h>
10#include <stddef.h>
11#include <stdint.h>
12#include <stdio.h>
13#include <string.h>
14#include <ucontext.h>
15
16#include "ap_map.h"
17#include "ap_plan.h"
18#include "ap_snes.h"
19
20// ---- assert_bp ------------------------------------------------------------
21//
22// ap_macro.h: `#define assert_bp(x) ({ if (!(x)) { __asm__("int3"); ... } })`.
23// Upstream ran under gdb, where int3 stops and `continue` resumes at the next
24// instruction. Outside a debugger the kernel turns int3 into SIGTRAP, whose
25// default action kills the process. This handler is the `continue`: it counts
26// the trap, remembers where it was, and returns - x86 int3 is a trap, so the
27// saved instruction pointer is already past it and execution goes on exactly
28// as gdb's would.
29
30static volatile uint64_t zb_trap_count;
31static volatile uint64_t zb_trap_ip;
32static volatile uint64_t zb_trap_frame;
33
34static void
35zb_on_trap(int sig, siginfo_t * info, void * context)
36{
37    (void) sig;
38    (void) info;
39    ucontext_t * uc = context;
40    zb_trap_count++;
41    zb_trap_ip = (uint64_t) uc->uc_mcontext.gregs[REG_RIP];
42    zb_trap_frame = ap_frame;
43}
44
45void
46zb_install_trap_handler(void)
47{
48    struct sigaction sa;
49    memset(&sa, 0, sizeof sa);
50    sa.sa_sigaction = zb_on_trap;
51    sa.sa_flags = SA_SIGINFO | SA_RESTART;
52    sigemptyset(&sa.sa_mask);
53    sigaction(SIGTRAP, &sa, NULL);
54}
55
56uint64_t zb_traps(void) { return zb_trap_count; }
57uint64_t zb_last_trap_ip(void) { return zb_trap_ip; }
58uint64_t zb_last_trap_frame(void) { return zb_trap_frame; }
59
60// Where an address in this process lies relative to a symbol we know, so a
61// trap's instruction pointer can be resolved with addr2line against the
62// binary: ip - zb_anchor() + (the anchor's address in `nm`).
63uintptr_t zb_anchor(void) { return (uintptr_t) &ap_tick; }
64
65// ---- where the bot's files go ------------------------------------------
66//
67// //third-party/c:zbanks-alttp is compiled with -Dfopen=zb_fopen and
68// -Drename=zb_rename (see its BUCK). A relative name is taken relative to
69// the directory the host set here, instead of the process's working
70// directory; with none set, nothing changes. This file is compiled WITHOUT
71// those defines, so fopen and rename below are libc's.
72
73#include <limits.h>
74#include <stdlib.h>
75
76static char zb_output_dir[PATH_MAX];
77
78void
79zb_set_output_dir(const char * dir)
80{
81    snprintf(zb_output_dir, sizeof zb_output_dir, "%s", dir ? dir : "");
82}
83
84static const char *
85zb_path(const char * name, char * buf, size_t len)
86{
87    if (name[0] == '/' || zb_output_dir[0] == '\0') return name;
88    snprintf(buf, len, "%s/%s", zb_output_dir, name);
89    return buf;
90}
91
92FILE *
93zb_fopen(const char * name, const char * mode)
94{
95    char buf[PATH_MAX];
96    return fopen(zb_path(name, buf, sizeof buf), mode);
97}
98
99int
100zb_rename(const char * from, const char * to)
101{
102    char a[PATH_MAX], b[PATH_MAX];
103    return rename(zb_path(from, a, sizeof a), zb_path(to, b, sizeof b));
104}
105
106void zb_flush_stdout(void) { fflush(stdout); }
107
108// ---- reading the bot's state ---------------------------------------------
109
110const char * zb_info_string(void) { return ap_info_string; }
111
112// Link where the bot thinks he is (its own coordinate system: indoors is
113// x >= 0x4000, see ap_math.h XYINDOORS).
114void
115zb_link_xy(uint16_t * x, uint16_t * y)
116{
117    struct xy xy = ap_link_xy();
118    *x = xy.x;
119    *y = xy.y;
120}
121
122// The task list, one task per line, in ap_print_task's own words - what the
123// bot is doing now, first line first.
124size_t
125zb_tasks(char * buf, size_t len)
126{
127    size_t used = 0;
128    if (len == 0) return 0;
129    buf[0] = '\0';
130    for (struct ap_task * t = ap_task_list->next; t != ap_task_list; t = t->next) {
131        int n = snprintf(buf + used, len - used, "%s\n", ap_print_task(t));
132        if (n < 0 || (size_t) n >= len - used) {
133            used = len - 1;
134            break;
135        }
136        used += (size_t) n;
137    }
138    return used;
139}
140
141// The goal list: type, node, screen, attempts, and the score it was given the
142// last time the planner scored it - in list order, which is the planner's.
143size_t
144zb_goals(char * buf, size_t len, size_t max_goals)
145{
146    size_t used = 0;
147    size_t count = 0;
148    if (len == 0) return 0;
149    buf[0] = '\0';
150    for (struct ap_goal * g = ap_goal_list->next; g != ap_goal_list && count < max_goals; g = g->next, count++) {
151        const char * screen = (g->node && g->node->screen) ? g->node->screen->name : "";
152        int n = snprintf(buf + used, len - used, "%s\t%s\t%s\t%d\t%d\n",
153                ap_goal_type_names[g->type], g->node ? g->node->name : "(null)",
154                screen, g->attempts, g->last_score);
155        if (n < 0 || (size_t) n >= len - used) {
156            used = len - 1;
157            break;
158        }
159        used += (size_t) n;
160    }
161    return used;
162}
163
164size_t
165zb_goal_count(void)
166{
167    size_t count = 0;
168    for (struct ap_goal * g = ap_goal_list->next; g != ap_goal_list; g = g->next) count++;
169    return count;
170}
171
172// ---- the goal choice, for a host that wants to make it ------------------
173//
174// Carried patch 0002 (third-party/c/patches/0002-goal-choice-hook.patch) gives
175// ap_goal_evaluate a hook, `ap_goal_choose_hook`, NULL by default. When the
176// host asks for it (zb_set_goal_chooser), the hook installed here gathers the
177// satisfiable goals whose score is within `margin` of upstream's own lowest,
178// cheapest first and at most ZB_MAX_OPTIONS of them, describes each from the
179// bot's own records, and lets the host's function pick one by index. With
180// fewer than two such goals, or when the host answers out of range, it
181// returns upstream's pick unchanged: the choice only moves when there was a
182// real choice to make.
183
184#include "ap_item.h"
185#include "ap_req.h"
186
187#define ZB_MAX_OPTIONS 6
188
189extern struct ap_goal * (*ap_goal_choose_hook)(struct ap_goal * min_goal, int min_score,
190        int (*score)(struct ap_goal * goal, int max_score));
191
192// One goal, as the host sees it. Strings are valid only during the call.
193struct zb_goal_option {
194    const char * kind;      // ap_goal_type_names: PICKUP CHEST EXPLORE ITEM NPC SCRIPT
195    const char * node;      // the node's own name, e.g. "door U 0x84 DOOR|NODE NORM"
196    const char * screen;    // the screen's name, e.g. "Well Uncle ^ 0 6a00,a00 x 6bff,aff"
197    uint16_t screen_id;
198    uint8_t indoors;        // the screen is inside (the bot's x >= 0x4000, ap_math.h)
199    uint8_t direction;      // an exit's direction: dir_names index, 0 if none
200    uint8_t sprite_type;    // NPC/ITEM sprite, 0 if none
201    uint16_t sprite_subtype;
202    const char * item;      // what the bot believes is there, "" if unknown
203    const char * script;    // a script's move sequence, "" if none
204    const char * needs;     // ap_req_print: the requirements it was given
205    int score;              // ap_goal_score now: lower is sooner
206    int distance;           // the part of the score that is path length
207    int attempts;           // times it has failed already
208    uint8_t adjacent_known; // an exit whose other side the bot has mapped
209    // Where it is, in the bot's coordinates (ap_math.h: indoors is
210    // x >= 0x4000): the node's centre and its screen's bounds.
211    uint16_t x, y;
212    uint16_t screen_x0, screen_y0, screen_x1, screen_y1;
213    // The bot's own path to it (ap_pathfind_global's `from` chain): the exit
214    // it leaves Link's screen by ("" when the goal is on Link's screen) and
215    // that exit's direction, and how many screens the path enters on the way.
216    const char * via;
217    uint8_t via_direction;
218    uint8_t screens_on_path;
219    // Bit j set: this goal's node lies on option j's path, i.e. doing it is
220    // on the way to option j.
221    uint8_t on_way_to;
222};
223
224// Where Link is when the choice is made.
225struct zb_goal_context {
226    const char * link_screen; // the screen's name, "" if the bot has none
227    uint8_t link_indoors;
228    uint8_t has_sword;
229    uint16_t link_x, link_y;  // ap_link_xy
230    uint16_t screen_x0, screen_y0, screen_x1, screen_y1; // Link's screen, 0 if none
231};
232
233typedef int (*zb_goal_chooser)(const struct zb_goal_option * options, size_t count,
234        const struct zb_goal_context * context);
235
236static zb_goal_chooser zb_chooser;
237static int zb_margin;
238
239static const char *
240zb_screen_name(const struct ap_node * node)
241{
242    return (node && node->screen) ? node->screen->name : "";
243}
244
245// The path ap_goal_score just found to `node`, destination first, as the
246// search left it in each node's `pgsearch.from` (ap_map.c:1515-1531, 1566-1610):
247// valid until the next search, so it is read straight after each score call.
248#define ZB_MAX_PATH 48
249struct zb_path {
250    const struct ap_node * nodes[ZB_MAX_PATH];
251    size_t length;
252};
253
254static void
255zb_read_path(const struct ap_node * node, struct zb_path * path)
256{
257    path->length = 0;
258    for (const struct ap_node * n = node; n != NULL && path->length < ZB_MAX_PATH; n = n->pgsearch.from) {
259        path->nodes[path->length++] = n;
260        if (n->type == NODE_NONE) break; // the search's start: Link
261    }
262}
263
264static struct ap_goal *
265zb_choose(struct ap_goal * min_goal, int min_score, int (*score)(struct ap_goal *, int))
266{
267    if (zb_chooser == NULL) return min_goal;
268    // The costs that are not path: upstream's ap_goal_score (ap_plan.c:758-760).
269    const bool swordless = *ap_ram.inventory_sword == 0;
270
271    struct ap_goal * picked[ZB_MAX_OPTIONS];
272    int scores[ZB_MAX_OPTIONS];
273    static struct zb_path paths[ZB_MAX_OPTIONS];
274    static struct zb_path path;
275    size_t n = 0;
276    int limit = min_score + zb_margin;
277    if (limit < min_score) limit = min_score; // overflow: margin 0
278    // min_goal first: it is upstream's pick, and the default. Scored again
279    // only to read its path; the score itself is upstream's.
280    picked[n] = min_goal;
281    scores[n] = min_score;
282    paths[n].length = 0;
283    if (min_goal->node != NULL && score(min_goal, limit + 1) >= 0) zb_read_path(min_goal->node, &paths[n]);
284    n++;
285    for (struct ap_goal * g = ap_goal_list->next; g != ap_goal_list; g = g->next) {
286        if (g == min_goal) continue;
287        // Unsatisfiable and complete were decided in upstream's loop just now.
288        if (g->last_score == INT_MAX - 1 || g->last_score == -2) continue;
289        int s = score(g, limit + 1);
290        if (s < 0 || s > limit) continue;
291        // Insert in score order, keeping the ZB_MAX_OPTIONS cheapest.
292        size_t at = n;
293        while (at > 1 && scores[at - 1] > s) at--;
294        if (at >= ZB_MAX_OPTIONS) continue;
295        path.length = 0;
296        if (g->node != NULL) zb_read_path(g->node, &path);
297        size_t end = n < ZB_MAX_OPTIONS ? n : ZB_MAX_OPTIONS - 1;
298        for (size_t i = end; i > at; i--) {
299            picked[i] = picked[i - 1];
300            scores[i] = scores[i - 1];
301            paths[i] = paths[i - 1];
302        }
303        picked[at] = g;
304        scores[at] = s;
305        paths[at] = path;
306        if (n < ZB_MAX_OPTIONS) n++;
307    }
308    if (n < 2) return min_goal;
309    struct xy link = ap_link_xy();
310    struct ap_screen * here = ap_update_map_screen(false);
311
312    struct zb_goal_option options[ZB_MAX_OPTIONS];
313    char needs[ZB_MAX_OPTIONS][128];
314    for (size_t i = 0; i < n; i++) {
315        struct ap_goal * g = picked[i];
316        const struct ap_node * node = g->node;
317        snprintf(needs[i], sizeof needs[i], "%s", ap_req_print(&g->req));
318        int base = g->attempts * 100 + ((swordless && g->type != GOAL_NPC) ? 10000 : 0);
319        options[i] = (struct zb_goal_option) {
320            .kind = ap_goal_type_names[g->type],
321            .node = node ? node->name : "",
322            .screen = zb_screen_name(node),
323            .screen_id = (node && node->screen) ? node->screen->id : 0,
324            .indoors = (node && node->screen && node->screen->tl.x >= 0x4000) ? 1 : 0,
325            .direction = node ? node->adjacent_direction : 0,
326            .sprite_type = node ? node->sprite_type : 0,
327            .sprite_subtype = node ? node->sprite_subtype : 0,
328            .item = (node && node->item_loc && node->item_loc->item) ? node->item_loc->item->name : "",
329            .script = (node && node->script && node->script->sequence) ? node->script->sequence : "",
330            .needs = needs[i],
331            .score = scores[i],
332            .distance = scores[i] - base,
333            .attempts = g->attempts,
334            .adjacent_known = (node && node->adjacent_node) ? 1 : 0,
335        };
336        struct zb_goal_option * o = &options[i];
337        o->via = "";
338        if (node) {
339            o->x = (uint16_t) ((node->tl.x + node->br.x) / 2);
340            o->y = (uint16_t) ((node->tl.y + node->br.y) / 2);
341        }
342        if (node && node->screen) {
343            o->screen_x0 = node->screen->tl.x;
344            o->screen_y0 = node->screen->tl.y;
345            o->screen_x1 = node->screen->br.x;
346            o->screen_y1 = node->screen->br.y;
347        }
348        // Walk the path from Link's end: the first node off the start that is
349        // on Link's screen and leads elsewhere is the exit taken; every change
350        // of screen after it is a screen entered.
351        const struct zb_path * p = &paths[i];
352        const struct ap_screen * last = here;
353        for (size_t k = p->length; k-- > 0;) {
354            const struct ap_node * step = p->nodes[k];
355            if (step->type == NODE_NONE) continue;
356            if (o->via[0] == '\0' && step != node && step->screen == here && step->adjacent_direction != 0) {
357                o->via = step->name;
358                o->via_direction = step->adjacent_direction;
359            }
360            if (step->screen != last) {
361                last = step->screen;
362                if (o->screens_on_path < UINT8_MAX) o->screens_on_path++;
363            }
364        }
365        // This goal's node on another option's path: it is on the way there.
366        for (size_t j = 0; j < n; j++) {
367            if (j == i || node == NULL) continue;
368            for (size_t k = 0; k < paths[j].length; k++) {
369                if (paths[j].nodes[k] == node && paths[j].nodes[k] != picked[j]->node) {
370                    o->on_way_to |= (uint8_t) (1u << j);
371                    break;
372                }
373            }
374        }
375    }
376    struct zb_goal_context context = {
377        .link_screen = here ? here->name : "",
378        .link_indoors = link.x >= 0x4000,
379        .has_sword = !swordless,
380        .link_x = link.x,
381        .link_y = link.y,
382        .screen_x0 = here ? here->tl.x : 0,
383        .screen_y0 = here ? here->tl.y : 0,
384        .screen_x1 = here ? here->br.x : 0,
385        .screen_y1 = here ? here->br.y : 0,
386    };
387    int chosen = zb_chooser(options, n, &context);
388    if (chosen < 0 || (size_t) chosen >= n) return min_goal;
389    return picked[chosen];
390}
391
392// Install (chooser != NULL) or remove the host's goal chooser. `margin` is
393// how far above upstream's lowest score a goal may be and still be offered.
394void
395zb_set_goal_chooser(zb_goal_chooser chooser, int margin)
396{
397    zb_chooser = chooser;
398    zb_margin = margin < 0 ? 0 : margin;
399    ap_goal_choose_hook = chooser ? zb_choose : NULL;
400}
401
402// ---- goals the save says are done ----------------------------------------
403//
404// Every goal the bot makes, it makes in ap_map.c as it adds a node, and
405// ap_map.c is compiled with -Dap_goal_add=zb_goal_add (third-party/c/BUCK),
406// so it comes through here. Upstream only ever started from its "home", a
407// fresh save, so each node it imported (map.19.txt, alttp.c:31-38) was still
408// to do. We also start mid-game (the window's resume, a later run's home), and
409// then an imported NPC can be done already. Chests and pots the bot re-checks
410// against the save itself; an NPC it cannot. Uncle is the one that breaks it:
411// he is gone once $7EF3C6 bit 0 is set (SpritePrep_Uncle, usdasm
412// bank_05.asm:16216-16240), TALK_NPC then reads the stale $02D8 - the last
413// item Link received - as uncle's gift, and the item tracker asserts that
414// uncle's sword is that item (ap_item.c:518) and aborts (run o1, frame
415// 21,758). A goal like that is not created.
416
417struct ap_goal * ap_goal_add(enum ap_goal_type type, struct ap_node * node);
418
419static bool
420zb_done_in_this_save(enum ap_goal_type type, const struct ap_node * node)
421{
422    if (type != GOAL_NPC || node == NULL) return false;
423    const uint8_t progress_flags = *(const uint8_t *) ap_emu->base(0x7EF3C6);
424    return node->sprite_type == 0x73 && node->sprite_subtype == 0x100 && (progress_flags & 0x01);
425}
426
427// Room $0123's "Mini Moldorm Cave Guy" (sprite type 0xBB, subtype 0x0200) is
428// a member of upstream's own "Salesman / chestgame guy / 300 rupee giver guy
429// / Chest game thief / Item for sale" family (ap_snes.h) - it gates its
430// gift behind a choice or a cost (a payment prompt, a chest game, a
431// kill-4-mini-moldorms precondition) that TALK_NPC's gift detection (a
432// plain WRAM-item-slot change) can never see, whatever this specific room
433// actually requires. Recovery already gives up on the resulting stall
434// correctly (research/zbanks-alttp.md, "The shopkeeper cannot be paid"),
435// but not before spending its whole cap doing so: measured 2026-09-22, this
436// one goal alone exhausted Recovery's cap (5 attempts, frames 91483-100797)
437// and ended a 400,000-frame headless run at frame 104,397 - a quarter of
438// the budget - while "Library Book of Mudora" sat the whole time as a
439// correctly satisfiable, in-path-range goal (goals.txt's own "limit", not
440// "unsat") that the greedy scorer never got the chance to reach. Declining
441// the goal outright, the same way uncle's done-in-this-save goal is
442// declined above, spends nothing trying what cannot succeed - the bot may
443// still enter, explore and take chests/pots in this room, only this one
444// NPC's TALK_NPC goal is withheld.
445static bool
446zb_npc_cannot_be_satisfied(enum ap_goal_type type, const struct ap_node * node)
447{
448    if (type != GOAL_NPC || node == NULL || node->screen == NULL) return false;
449    return node->sprite_type == 0xBB && node->sprite_subtype == 0x0200
450        && node->screen->dungeon_room == 0x0123;
451}
452
453// ---- NPC goals whose reward the save already shows ------------------------
454//
455// Unlike a chest (GOAL_CHEST reads sram_room_state, ap_map.c) a talk-for-item
456// NPC has no room-state bit the bot's own scoring re-checks, so nothing
457// stops the bot walking back to one whose reward is already held: Jev sent
458// the bot back to Sahasrahla with Pegasus Boots already in hand (user,
459// 2026-09-22, live, frame 3806, right after a restart rebuilt the goal list
460// from nothing). Table-driven, one row per known TALK|NODE-override NPC
461// (ap_snes.h's room-scoped `ap_sprite_attrs_for_type` table has exactly
462// three: Sahasrahla, uncle, and room $0123's shopkeeper, handled separately
463// above since ITS block is "can never succeed", not "already succeeded") -
464// the next one-time NPC reward is a row here, not a new function.
465struct zb_npc_reward {
466    uint8_t sprite_type;
467    uint16_t sprite_subtype;
468    uint32_t addr;   // WRAM address that reads nonzero once the reward is held.
469    const char * why;
470};
471
472static const struct zb_npc_reward zb_npc_rewards[] = {
473    // Sahasrahla (sprite $16/$0000): Pegasus Boots. $7EF355 = inventory_base
474    // ($7EF33F) + BOOTS's own index (0x16) - cross-checked against
475    // research/alttp-ram-map.md's own citation (zelda3 variables.h:1085,
476    // "link_item_boots").
477    { .sprite_type = 0x16, .sprite_subtype = 0x0000, .addr = 0x7EF355, .why = "boots already held" },
478    // Uncle (sprite $73/$0100): the sword. A second, independent check
479    // alongside zb_done_in_this_save's own crash-avoidance one above - that
480    // one exists to stop a stale-memory assert once uncle's own SpritePrep
481    // bit fires; this one stops a wasted trip once the sword is already
482    // held, which can be true before that bit is (inventory_sword, $7EF359).
483    { .sprite_type = 0x73, .sprite_subtype = 0x0100, .addr = 0x7EF359, .why = "sword already held" },
484};
485
486static bool
487zb_npc_reward_already_held(enum ap_goal_type type, const struct ap_node * node, const char ** why_out)
488{
489    if (type != GOAL_NPC || node == NULL) return false;
490    for (size_t i = 0; i < sizeof(zb_npc_rewards) / sizeof(*zb_npc_rewards); i++) {
491        const struct zb_npc_reward * r = &zb_npc_rewards[i];
492        if (node->sprite_type != r->sprite_type || node->sprite_subtype != r->sprite_subtype) continue;
493        if (*(const uint8_t *) ap_emu->base(r->addr) != 0) {
494            *why_out = r->why;
495            return true;
496        }
497        break;
498    }
499    return false;
500}
501
502struct ap_goal *
503zb_goal_add(enum ap_goal_type type, struct ap_node * node)
504{
505    if (zb_done_in_this_save(type, node)) {
506        printf("zbanks host: not adding goal for %s: done in this save\n", node->name);
507        return NULL;
508    }
509    if (zb_npc_cannot_be_satisfied(type, node)) {
510        printf("zbanks host: not adding goal for %s: cannot be satisfied (shopkeeper family, room $0123)\n", node->name);
511        return NULL;
512    }
513    const char * reward_why = NULL;
514    if (zb_npc_reward_already_held(type, node, &reward_why)) {
515        printf("zbanks host: not adding goal for %s: reward already held (%s)\n", node->name, reward_why);
516        return NULL;
517    }
518    struct ap_goal * goal = ap_goal_add(type, node);
519    // ap_manual_mode is a one-way latch (ap_plan.c:917, "No goals available;
520    // falling back to manual mode") and alttp.c's ap_tick returns before
521    // ever reaching ap_goal_evaluate again once it is set - forever, on
522    // every future frame, whatever the goal list holds. zb_retry_given_up
523    // below calls back into this same function to put a given-up goal back
524    // on the list, so a bot that had already given up entirely (every goal
525    // unsatisfiable, e.g. the live window's frame 43167: an unrelated
526    // NPC/pot exploration exhausted every currently-reachable node) put its
527    // own pad down for good and never looked at the retried goal, even
528    // though the host's whole point in retrying was to give it something
529    // new to do. This is every call site ap_map.c has for adding a goal
530    // (third-party/c/BUCK: -Dap_goal_add=zb_goal_add), fresh discovery
531    // during ordinary exploration included, so clearing the latch here
532    // covers both, not just the retry path.
533    if (goal != NULL && ap_manual_mode) {
534        printf("zbanks host: a goal was added while in manual mode; resuming the plan\n");
535        ap_manual_mode = false;
536    }
537    return goal;
538}
539
540// ---- goals the bot gave up on, for a host that wants to retry them -------
541//
542// ap_goal_fail (ap_plan.c:917-926) counts a failed attempt and, past three,
543// takes the goal off ap_goal_list for good (LL_EXTRACT; the goal is never
544// freed). Nothing upstream ever puts one back - its TODO still has "Reset
545// goal attempts count when re-visiting screens (maybe a bad idea?)", and no
546// commit after it did - so a goal tried before it could work is lost: the
547// Eastern Palace big chest, tried four times before the big key (run b5).
548//
549// zb_watch_goals, called after every tick, remembers the goals on the list;
550// one that has left it with attempts > 3 was permafailed (a completed goal
551// leaves with attempts <= 3, ap_goal_complete). zb_retry_given_up, called
552// when the host decides Link's means have changed, re-adds each through the
553// bot's own ap_goal_add - a fresh goal, attempts 0, requirements exactly as
554// upstream sets them - so the bot scores and pursues it like any other.
555
556#include <stdlib.h>
557
558#define ZB_MAX_GOALS 8192
559static struct ap_goal * zb_seen[ZB_MAX_GOALS];
560static size_t zb_n_seen;
561static struct ap_goal * zb_given_up[ZB_MAX_GOALS];
562static size_t zb_n_given_up;
563// Goals that left the list with attempts <= 3 - genuinely completed
564// (ap_goal_complete's own LL_EXTRACT), not permafailed. A `Recovery`'s own
565// progress signal (packages/zbanks/src/recovery.rs) alongside possessions:
566// a goal finishing is evidence a retry actually helped. Same threshold as
567// the given-up test below; keep them together on any change
568// (packages/zbanks/CLAUDE.md).
569static uint64_t zb_n_completed;
570
571// ---- per-goal outcome log, for Jev's own history (Facts) -------------------
572//
573// zb_watch_goals already tells "gone" apart into given-up (permafailed,
574// attempts > 3) and completed (attempts <= 3); this remembers WHICH goal, as
575// the identical "kind|node|screen" identity Rust's own identity() computes
576// (packages/decisions/src/goal_choice.rs) - so a later Ask can look a
577// re-offered option's own past straight up instead of asking Jev to guess
578// whether it is a repeat of a known failure (frame 23806: Jev picked a
579// routine that had already failed here, with nothing in the request saying
580// so). A ring, not a growing list: the C side's own memory does not survive
581// a restart anyway (a fresh bot remembers nothing), so only "since the last
582// drain" needs to fit here - packages/zbanks/src/lib.rs drains it every
583// tick, the same cadence zb_watch_goals already runs at, into its own
584// persisted, deduplicated-by-identity log (next to map_export.txt, reloaded
585// at startup - that persistence is the whole point, and it is entirely the
586// Rust side's job).
587#define ZB_MAX_OUTCOMES 256
588struct zb_raw_outcome {
589    char identity[192]; // "{kind}|{node}|{screen}", goal_choice::identity()'s own format
590    int attempts;
591    int completed; // 1 completed, 0 given up (permafailed)
592    uint32_t frame;
593};
594static struct zb_raw_outcome zb_outcomes[ZB_MAX_OUTCOMES];
595static size_t zb_n_outcomes;
596
597static void
598zb_note_outcome(const struct ap_goal * g, bool completed)
599{
600    if (zb_n_outcomes >= ZB_MAX_OUTCOMES) {
601        // The ring is full only if nothing has drained it in a very long
602        // time - drop the oldest, since the caller's own persisted log
603        // (Rust side) is what is meant to be durable, not this buffer.
604        memmove(&zb_outcomes[0], &zb_outcomes[1], (ZB_MAX_OUTCOMES - 1) * sizeof zb_outcomes[0]);
605        zb_n_outcomes--;
606    }
607    struct zb_raw_outcome * o = &zb_outcomes[zb_n_outcomes++];
608    snprintf(o->identity, sizeof o->identity, "%s|%s|%s",
609        ap_goal_type_names[g->type], g->node ? g->node->name : "", zb_screen_name(g->node));
610    o->attempts = g->attempts;
611    o->completed = completed ? 1 : 0;
612    o->frame = (uint32_t) ap_frame;
613}
614
615// Drains up to `max` outcomes into `out`, oldest first, and forgets them -
616// packages/zbanks/src/lib.rs owns folding them into its persisted log, so
617// nothing here needs to remember what was already handed out.
618size_t
619zb_drain_goal_outcomes(struct zb_raw_outcome * out, size_t max)
620{
621    size_t n = zb_n_outcomes < max ? zb_n_outcomes : max;
622    memcpy(out, zb_outcomes, n * sizeof *out);
623    zb_n_outcomes -= n;
624    if (zb_n_outcomes > 0) {
625        memmove(zb_outcomes, zb_outcomes + n, zb_n_outcomes * sizeof *out);
626    }
627    return n;
628}
629
630static int
631zb_pointer_order(const void * a, const void * b)
632{
633    uintptr_t x = (uintptr_t) *(struct ap_goal * const *) a;
634    uintptr_t y = (uintptr_t) *(struct ap_goal * const *) b;
635    return (x > y) - (x < y);
636}
637
638// Returns how many goals were newly found given up.
639size_t
640zb_watch_goals(void)
641{
642    static struct ap_goal * now[ZB_MAX_GOALS];
643    size_t n = 0;
644    for (struct ap_goal * g = ap_goal_list->next; g != ap_goal_list && n < ZB_MAX_GOALS; g = g->next) {
645        now[n++] = g;
646    }
647    qsort(now, n, sizeof now[0], zb_pointer_order);
648    size_t newly = 0;
649    for (size_t i = 0; i < zb_n_seen; i++) {
650        struct ap_goal * g = zb_seen[i];
651        if (bsearch(&g, now, n, sizeof now[0], zb_pointer_order) != NULL) continue;
652        // Gone. Permafailed, or completed (ap_goal_fail's own threshold,
653        // attempts > 3)? Only a node goal can be re-added either way
654        // (ap_goal_add takes a node; GOAL_ITEM has none), so a permafailed
655        // GOAL_ITEM or node-less goal is neither retried nor counted as
656        // completed - it is simply gone.
657        if (g->attempts > 3) {
658            if (g->node != NULL && g->type != GOAL_ITEM && zb_n_given_up < ZB_MAX_GOALS) {
659                zb_given_up[zb_n_given_up++] = g;
660                newly++;
661            }
662        } else {
663            zb_n_completed++;
664        }
665    }
666    memcpy(zb_seen, now, n * sizeof now[0]);
667    zb_n_seen = n;
668    return newly;
669}
670
671size_t zb_given_up_count(void) { return zb_n_given_up; }
672uint64_t zb_completed_count(void) { return zb_n_completed; }
673
674// Put every given-up goal back, saying why in the bot's own log. Returns how
675// many went back (one already back on the list is not added twice).
676size_t
677zb_retry_given_up(const char * why)
678{
679    size_t added = 0;
680    for (size_t i = 0; i < zb_n_given_up; i++) {
681        struct ap_goal * old = zb_given_up[i];
682        struct ap_goal * fresh = zb_goal_add(old->type, old->node);
683        if (fresh == NULL) continue;
684        printf("zbanks host: retrying goal %s, given up after %d attempts, because %s\n",
685                fresh->name, old->attempts, why);
686        added++;
687    }
688    zb_n_given_up = 0;
689    return added;
690}