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}