1//! What the bot playing the game says about itself: the zbanks/alttp C bot 2//! (`packages/zbanks`), read from its own globals once a UI pass. 3//! 4//! Plain data in, so this crate needs neither the bot nor its C and still 5//! compiles for wasm; the app fills a [`Bot`] from `zbanks::Report`. 6 7use egui::{FontId, RichText}; 8 9use crate::palette::{FAINT, GOAL, GOOD, GREY, PLAIN, RED, SECTION}; 10 11/// One goal on the bot's goal list (`ap_plan.c`'s `struct ap_goal`). 12pub struct Goal<'a> { 13 /// `GOAL_*` without the prefix: EXPLORE, CHEST, PICKUP, NPC, ITEM, SCRIPT. 14 pub kind: &'a str, 15 /// The node the goal is about, in the bot's own naming. 16 pub node: &'a str, 17 /// The screen that node is on. 18 pub screen: &'a str, 19 pub attempts: i32, 20 /// The score the planner last gave it, as words when it is not a cost 21 /// ("complete", "unsatisfiable", "over limit" - `zbanks::score_words`). 22 pub score: String, 23 /// Whether that score is one the planner will not pick. 24 pub out_of_reach: bool, 25 pub complete: bool, 26} 27 28pub struct Bot<'a> { 29 /// The frame last passed to `ap_tick`. 30 pub frame: u32, 31 /// `ap_info_string`: the line upstream's Snes9x drew over the picture. 32 pub info: &'a str, 33 /// The task list, current task first, in `ap_print_task`'s words. 34 pub tasks: &'a [String], 35 pub goals: &'a [Goal<'a>], 36 /// All goals the bot has, of which `goals` are the first. 37 pub goal_count: usize, 38 pub link: (u16, u16), 39 /// `assert_bp` failures so far (each a trap the host stepped over, as gdb 40 /// would), and the frame of the last. 41 pub traps: u64, 42 pub last_trap_frame: u64, 43 /// `base()` requests that mapped to nothing; should stay 0. 44 pub stray_reads: u64, 45 /// Goals the bot gave up on, waiting for Link's possessions to change, 46 /// and how many the host has put back so far. 47 pub given_up: usize, 48 pub retried: u64, 49 /// Set by the host's stall detector (`packages/zbanks/src/stall.rs`): 50 /// the bot has not chosen a goal, or has not moved, for its threshold's 51 /// worth of frames, or the planner has given up for good. `None` means 52 /// not stalled - never omitted quietly, so the panel cannot look normal 53 /// while the bot has stopped playing. 54 pub stalled: Option<&'a str>, 55 /// The host's own recovery from a stall (`packages/zbanks/src/recovery.rs`): 56 /// the last attempt's line, and whether recovery has given up for good 57 /// (shown in red, like `stalled`, instead of the ordinary attention 58 /// colour) - once true it stays true for the life of the process, same 59 /// as `stalled`'s own `ManualMode` used to before recovery could clear 60 /// it. 61 pub recovery: Option<(&'a str, bool)>, 62 /// Held on the keyboard: the bot steps aside (upstream alttp.c:57). 63 pub paused_by_x: bool, 64} 65 66/// A small header used by more than one panel in this crate. `pub(crate)` 67/// so `jev.rs`'s "History" section can use the exact same look. 68pub(crate) fn section(ui: &mut egui::Ui, title: &str) { 69 ui.add_space(4.0); 70 ui.label(RichText::new(title).color(SECTION).strong()); 71} 72 73/// The bot's line, its tasks and its goals. 74pub fn show(ui: &mut egui::Ui, bot: &Bot) { 75 ui.heading("Bot"); 76 if let Some(stalled) = bot.stalled { 77 ui.add(egui::Label::new(RichText::new(stalled).color(RED).strong()).wrap()); 78 } 79 if let Some((line, gave_up)) = bot.recovery { 80 let colour = if gave_up { RED } else { GOAL }; 81 ui.add(egui::Label::new(RichText::new(line).color(colour).strong()).wrap()); 82 } 83 ui.label(RichText::new("zbanks/alttp, C").color(GREY)); 84 let info = if bot.info.is_empty() { "(nothing yet)" } else { bot.info }; 85 ui.add(egui::Label::new(RichText::new(info).font(FontId::monospace(12.0)).color(GOAL)).wrap()); 86 if bot.paused_by_x { 87 ui.label(RichText::new("X held: the bot steps aside, the pad is yours").color(RED)); 88 } 89 let mut facts = format!( 90 "frame {} link {:#06x},{:#06x} goals {}", 91 bot.frame, bot.link.0, bot.link.1, bot.goal_count 92 ); 93 if bot.traps > 0 { 94 facts.push_str(&format!(" traps {} (last frame {})", bot.traps, bot.last_trap_frame)); 95 } 96 if bot.given_up > 0 || bot.retried > 0 { 97 facts.push_str(&format!(" given up {} (put back {})", bot.given_up, bot.retried)); 98 } 99 ui.label(RichText::new(facts).color(GREY)); 100 if bot.stray_reads > 0 { 101 ui.label(RichText::new(format!("{} reads of unmapped memory", bot.stray_reads)).color(RED)); 102 } 103 104 egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| { 105 section(ui, "Tasks"); 106 if bot.tasks.is_empty() { 107 ui.label(RichText::new("none").color(GREY)); 108 } 109 for (i, task) in bot.tasks.iter().enumerate() { 110 let colour = if i == 0 { GOOD } else { PLAIN }; 111 ui.add( 112 egui::Label::new(RichText::new(task).font(FontId::monospace(11.0)).color(colour)) 113 .wrap(), 114 ); 115 } 116 section(ui, "Goals"); 117 for goal in bot.goals { 118 let colour = if goal.complete { 119 GOOD 120 } else if goal.out_of_reach { 121 FAINT 122 } else { 123 PLAIN 124 }; 125 let line = format!( 126 "{:<7} {:>12} {} on {}{}", 127 goal.kind, 128 goal.score, 129 goal.node, 130 goal.screen, 131 if goal.attempts > 0 { format!(" ({} tries)", goal.attempts) } else { String::new() }, 132 ); 133 ui.add(egui::Label::new(RichText::new(line).font(FontId::monospace(11.0)).color(colour)).wrap()); 134 } 135 if bot.goal_count > bot.goals.len() { 136 ui.label( 137 RichText::new(format!("... and {} more", bot.goal_count - bot.goals.len())).color(GREY), 138 ); 139 } 140 }); 141}