diff --git a/crates/benilla-app/src/ui_quest/mod.rs b/crates/benilla-app/src/ui_quest/mod.rs index 246cd6ea..88fea20e 100644 --- a/crates/benilla-app/src/ui_quest/mod.rs +++ b/crates/benilla-app/src/ui_quest/mod.rs @@ -279,6 +279,53 @@ pub(crate) fn row_is_one_click(icon: u32) -> bool { type Pool = Vec<(u32, u32)>; /// One wire item row for Lua; the name and quality are `None` and white until the template lands. +/// Reward rows already named by [`warn_unresolved_reward_once`]. The snapshot is rebuilt every +/// frame the panel is open, so without this the warning would print at frame rate. +static UNRESOLVED_REWARDS: std::sync::Mutex> = + std::sync::Mutex::new(std::collections::BTreeSet::new()); + +/// A reward row that resolves to neither a name nor an icon paints as an **empty button**, which +/// is what grom photographed on "The Tower of Azora" (quest 41747): two scrolls offered and a +/// blank third box under them. +/// +/// Both halves failing at once is the fact worth naming, because they come from independent +/// sources: the name from the item template ([`Items::template`], which queries the server on a +/// cache miss) and the icon from `ItemDisplayInfo.dbc` through the wire's own `display_id`. A +/// merely late template still paints an icon. Neither answering means the triple itself carries +/// nothing we can resolve. +/// +/// An instrument and not a fix, deliberately: the reference does not filter these either. Its +/// DETAILS publisher (`0x500ef0`) zeroes the six choice slots at `0xbdf028`, copies exactly +/// `count` entries in with **no test on the item id** (`500f41`-`500f58`), and zero-fills the +/// rest — so a server padding its choice list makes the reference draw the same empty button. +/// Whether this is a padded wire or a template we failed to fetch cannot be told from the +/// painted frame, and the two want opposite fixes. One line at the next sighting says which. +fn warn_unresolved_reward_once(it: &QuestRewardItem, view: &QuestItemView) { + if view.name.is_some() || view.texture.is_some() { + return; + } + let Ok(mut seen) = UNRESOLVED_REWARDS.lock() else { + return; + }; + if !seen.insert((it.item_id, it.display_id)) { + return; + } + warn!( + "ui_quest: a reward row resolved to neither a name nor an icon, so it paints as an empty \ + button — item {} x{}, displayId {}. {}", + it.item_id, + it.count, + it.display_id, + if it.item_id == 0 { + "The wire sent an empty triple, so the server padded its reward list and the \ + reference draws the same empty button — not ours to fix." + } else { + "The wire named a real item, so this one is ours: either its template never came \ + back from the query, or `ItemDisplayInfo.dbc` has no row for that displayId." + } + ); +} + fn resolve_item( it: &QuestRewardItem, items: &Items, @@ -295,7 +342,7 @@ fn resolve_item( let link = name .as_ref() .map(|n| crate::ui_items::item_link(it.item_id, n, quality)); - QuestItemView { + let view = QuestItemView { name, texture, count: it.count, @@ -303,7 +350,9 @@ fn resolve_item( item_id: it.item_id, usable: true, // not computed; the stock frame tints unusable ones red link, - } + }; + warn_unresolved_reward_once(it, &view); + view } fn resolve_items(