From a811f50e869e333a6076392201bc3857edebf44b Mon Sep 17 00:00:00 2001 From: grom Date: Fri, 30 Oct 2026 15:55:06 -0600 Subject: [PATCH] Name a quest reward row that resolves to nothing, rather than painting an empty button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit grom photographed "The Tower of Azora" (quest 41747) offering Scroll of Stamina, Scroll of Spirit, and a third button with no icon and no name — an empty bordered box. A reward row's two halves 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, and the feed rebuilds its snapshot every frame the panel is open, so a template arriving a second later would repaint. **Neither half answering** means the triple itself carries nothing this client can resolve. Ruled out from here, so the next person does not re-run them: the decode is a count-prefixed run of triples (vmangos' own layout), so a mis-stride would have broken rows 1 and 2 as well; `ItemDisplayCatalog` is a `HashMap` keyed by row id, so the realm's custom 68xxx display range is not an indexing trap (its `ItemDisplayInfo.dbc` has 45,254 rows and a max id of 69,176, and 68793/68797 are both present); and the two scrolls that *did* paint carry vanilla display ids (1093, 2616) while the plausible third choices carry custom ones, which is suggestive but not evidence. This lands as an instrument rather than a fix because **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`), then zero-fills the rest — so a server padding its reward list makes the reference draw the same empty button. A padded wire and a template we failed to fetch look identical from the painted frame and want opposite fixes, so guessing between them is how this becomes a fourth wrong verdict. `warn_unresolved_reward_once` prints the item id, count and displayId once per distinct pair (the snapshot rebuilds per frame, so unguarded it would print at frame rate) and says which of the two it is: an `item_id` of 0 is a padded wire and not ours, a real id is. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- crates/benilla-app/src/ui_quest/mod.rs | 53 +++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) 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(