Item tooltips show the lock's key or skill line and a gift's giver (#1454)
Item tooltips lacked the lock's sub-line and a wrapped gift's giver. The reference: the lock check (`0x52bd57`) takes the Lock row's slots in place. The first slot picks the arm: a key names its item in white (`0x52bf30`), and a skill names the LockType and the required value, coloured by the walk at `0x5da5e0`. A blank LockType name prints blank; only a missing row reads `UNKNOWN` (`0x52bede`). A wrapped item (flag `0x8`) prints `ITEM_WRAPPED_BY` with `GIFTCREATOR`, from the object or, for the trade partner's slots, from the block (`0x534559`, `0x534574`). Real lockboxes use locks whose first slot is empty, so they show no sub-line, as in 1.12. The rows keep all eight slots, and both trade sides carry the wrapped word and the giver. Tests cover each arm, a blank name, and gifts in bags and trade. Fixes #1440 Fixes #1441 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1597,6 +1597,7 @@ fn seed_cooldown_filmstrip(
|
||||
cooldown: Some(((now_ms - fraction * span_ms) as i64, span_ms as u32, true)),
|
||||
readable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
flags: 0,
|
||||
already_bound: false,
|
||||
occupied_seats: None,
|
||||
@@ -1657,6 +1658,7 @@ fn seed_bag_window(
|
||||
cooldown: None,
|
||||
readable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
flags: 0,
|
||||
already_bound: false,
|
||||
occupied_seats: None,
|
||||
@@ -1751,6 +1753,7 @@ fn seed_equipped_bags(
|
||||
cooldown: None,
|
||||
readable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
flags: 0,
|
||||
already_bound: false,
|
||||
occupied_seats: None,
|
||||
|
||||
@@ -222,6 +222,22 @@ pub(crate) fn enchant_lines(
|
||||
lines
|
||||
}
|
||||
|
||||
/// The name behind a wrapped item's `ITEM_WRAPPED_BY` line: only an item carrying WRAPPED `0x8`
|
||||
/// takes the gift arm (`0x52b789`, `0x52e1b9`), which reads `ITEM_FIELD_GIFTCREATOR` (`0x52e32a`)
|
||||
/// and asks the name cache (`0x52e29d`). A zero guid, an unwrapped item or a name not yet
|
||||
/// answered gives `None`, and only the first two ask nothing.
|
||||
pub(crate) fn gift_creator_name(
|
||||
fields: &ObjectFields,
|
||||
names: &crate::names::NameCache,
|
||||
commands: &NetCommands,
|
||||
) -> Option<String> {
|
||||
if fields.item_flags()? & benilla_protocol::messages::ITEM_DYNFLAG_WRAPPED == 0 {
|
||||
return None;
|
||||
}
|
||||
let giver = fields.item_gift_creator()?;
|
||||
names.resolve_player(giver, commands).map(str::to_string)
|
||||
}
|
||||
|
||||
/// `0x5da2c0`: already bound when `ITEM_FIELD_FLAGS & 1` is set or a live enchant slot names a
|
||||
/// binding `SpellItemEnchantment` row (`0x5da300` to `0x5da320`). The enchant cursor's bind
|
||||
/// question (gate `0x495d60`) and the tooltip's Soulbound line both read it. It reads the raw
|
||||
@@ -1073,6 +1089,35 @@ mod tests {
|
||||
ObjectFields::from_pairs(&[(21, flags), (22, slot0)])
|
||||
}
|
||||
|
||||
/// The gift arm (`0x52e1b9`) asks the name cache for the gift creator of a wrapped item and no
|
||||
/// other: an unwrapped item, a zero guid and a name still in flight add no name.
|
||||
#[test]
|
||||
fn only_a_wrapped_item_names_its_gift_creator() {
|
||||
let (tx, rx) = crossbeam_channel::unbounded();
|
||||
let cmds = NetCommands(tx);
|
||||
let mut names = crate::names::NameCache::default();
|
||||
let giver =
|
||||
|flags: u32, guid: u32| ObjectFields::from_pairs(&[(21, flags), (12, guid), (13, 0)]);
|
||||
// Unwrapped, even with a guid set: no ask, no name.
|
||||
assert_eq!(gift_creator_name(&giver(0, 5), &names, &cmds), None);
|
||||
assert!(matches!(rx.try_recv(), Err(TryRecvError::Empty)));
|
||||
// Wrapped, no giver: nothing to ask.
|
||||
assert_eq!(gift_creator_name(&giver(0x8, 0), &names, &cmds), None);
|
||||
assert!(matches!(rx.try_recv(), Err(TryRecvError::Empty)));
|
||||
// Wrapped, the name in flight: asked once, no name yet.
|
||||
assert_eq!(gift_creator_name(&giver(0x8, 5), &names, &cmds), None);
|
||||
assert!(matches!(
|
||||
rx.try_recv(),
|
||||
Ok(ClientCommand::NameQuery { guid: 5 })
|
||||
));
|
||||
// Answered.
|
||||
names.insert_player(5, "Santa".to_string(), None);
|
||||
assert_eq!(
|
||||
gift_creator_name(&giver(0x8, 5), &names, &cmds).as_deref(),
|
||||
Some("Santa")
|
||||
);
|
||||
}
|
||||
|
||||
/// `0x5da2c0`: the Firestone shape binds and prints no line, so a predicate read off
|
||||
/// [`enchant_lines`] would call it unbound.
|
||||
#[test]
|
||||
|
||||
@@ -547,6 +547,7 @@ fn slot_view(
|
||||
.item_creator()
|
||||
.filter(|&g| g != 0)
|
||||
.and_then(|g| names.resolve_player(g, commands).map(str::to_string));
|
||||
let gift_creator = crate::items::gift_creator_name(obj, names, commands);
|
||||
// All seven enchant slots: our own gear streams as item objects, where an inspected player's
|
||||
// descriptor carries only two.
|
||||
let enchants = crate::items::enchant_lines(
|
||||
@@ -625,6 +626,7 @@ fn slot_view(
|
||||
bar_placeable,
|
||||
consumable,
|
||||
creator,
|
||||
gift_creator,
|
||||
enchants,
|
||||
duration_ms,
|
||||
})
|
||||
@@ -715,6 +717,7 @@ fn inventory_slots(
|
||||
bar_placeable: false,
|
||||
consumable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
// No ammo instance streams, only its template id: no enchants, no countdown.
|
||||
enchants: Vec::new(),
|
||||
duration_ms: None,
|
||||
|
||||
@@ -1786,6 +1786,7 @@ mod bind_confirm_tests {
|
||||
cooldown: None,
|
||||
readable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
flags: 0,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
|
||||
@@ -412,7 +412,7 @@ pub(super) fn feed_item_stats(
|
||||
commands: Res<NetCommands>,
|
||||
spells: Option<Res<crate::ui_action::Spells>>,
|
||||
// The caster the `$`-tokens read: its spell modifiers and its skills.
|
||||
(spell_mods, self_q, locks): CasterReads,
|
||||
(spell_mods, self_q): CasterReads,
|
||||
skill_lines: Option<Res<crate::ui_spellbook::SkillLines>>,
|
||||
// The `$z` token: the bind point's area (`SMSG_BINDPOINTUPDATE`), named through `AreaTable`.
|
||||
home_bind: Option<Res<crate::net::HomeBind>>,
|
||||
@@ -489,7 +489,7 @@ pub(super) fn feed_item_stats(
|
||||
} else {
|
||||
mod_sensitive.remove(&id);
|
||||
}
|
||||
let mut view = template_view(
|
||||
let view = template_view(
|
||||
&t,
|
||||
spell_res,
|
||||
Some(&spell_mods),
|
||||
@@ -503,9 +503,6 @@ pub(super) fn feed_item_stats(
|
||||
icons.as_deref(),
|
||||
&get,
|
||||
);
|
||||
// A lock id with no `Lock.dbc` row is no lock to the builder (`0x52bd6b`..
|
||||
// `0x52bda9`); with the catalog unloaded the id stands.
|
||||
view.lock_id = known_lock_id(view.lock_id, locks.as_deref().map(|l| &l.0));
|
||||
Some((id, view))
|
||||
})
|
||||
.collect()
|
||||
@@ -515,24 +512,12 @@ pub(super) fn feed_item_stats(
|
||||
}
|
||||
}
|
||||
|
||||
/// What the `$`-tokens read of the caster, and the `Lock.dbc` the LOCKED line needs a row from
|
||||
/// (`0x52bd6b`).
|
||||
/// What the `$`-tokens read of the caster.
|
||||
type CasterReads<'w, 's> = (
|
||||
Res<'w, crate::spell::SpellModifiers>,
|
||||
Query<'w, 's, &'static ObjectStore, With<SelfPlayer>>,
|
||||
Option<Res<'w, crate::go_templates::Locks>>,
|
||||
);
|
||||
|
||||
/// The template's lock id when `Lock.dbc` holds its row, else 0: the builder's LOCKED line starts
|
||||
/// from that row (`0x52bd57`..`0x52bd76`), and a missing one is no lock. With the catalog not
|
||||
/// loaded the id stands.
|
||||
fn known_lock_id(lock_id: u32, locks: Option<&benilla_formats::LockCatalog>) -> u32 {
|
||||
match locks {
|
||||
Some(l) if l.slots(lock_id).is_none() => 0,
|
||||
_ => lock_id,
|
||||
}
|
||||
}
|
||||
|
||||
/// The player state the tooltip's red requirement lines check: level, class, race, skills,
|
||||
/// proficiencies (`SMSG_SET_PROFICIENCY`) and reputation ranks (the DBC base plus the
|
||||
/// `SMSG_INITIALIZE_FACTIONS` standing), pushed on change.
|
||||
@@ -708,6 +693,7 @@ fn resolve_slot(
|
||||
rolls.enchants,
|
||||
),
|
||||
);
|
||||
let gift_creator = crate::items::gift_creator_name(fields, names, commands);
|
||||
// A container's filled seats, which `ClickTradeButton` counts (`0x4bff50`).
|
||||
let occupied_seats = fields.is_container().then(|| {
|
||||
let n = fields.container_num_slots().unwrap_or(0).min(36) as u8;
|
||||
@@ -732,6 +718,7 @@ fn resolve_slot(
|
||||
count,
|
||||
readable,
|
||||
creator,
|
||||
gift_creator,
|
||||
flags,
|
||||
already_bound,
|
||||
occupied_seats,
|
||||
@@ -778,6 +765,7 @@ fn resolve_slot(
|
||||
guid,
|
||||
readable,
|
||||
creator,
|
||||
gift_creator,
|
||||
flags,
|
||||
already_bound,
|
||||
occupied_seats,
|
||||
@@ -1484,23 +1472,6 @@ mod tests {
|
||||
BANK_CONTAINER, BANK_SLOTS,
|
||||
};
|
||||
|
||||
/// A lock id with no `Lock.dbc` row is no lock to the item builder (`0x52bd6b`..`0x52bda9`).
|
||||
#[test]
|
||||
fn a_lock_id_without_a_row_is_no_lock() {
|
||||
use benilla_formats::{LockCatalog, LockSlot, LOCK_KEY_ITEM};
|
||||
let mut slots = [LockSlot::default(); benilla_formats::MAX_LOCK_SLOTS];
|
||||
slots[0].key_type = LOCK_KEY_ITEM;
|
||||
let catalog = LockCatalog::from_rows([(7, slots)]);
|
||||
assert_eq!(super::known_lock_id(7, Some(&catalog)), 7);
|
||||
assert_eq!(super::known_lock_id(8, Some(&catalog)), 0);
|
||||
assert_eq!(
|
||||
super::known_lock_id(8, None),
|
||||
8,
|
||||
"no catalog, the id stands"
|
||||
);
|
||||
assert_eq!(super::known_lock_id(0, Some(&catalog)), 0);
|
||||
}
|
||||
|
||||
/// The view carries what `GetTradeSkillItemStats` reads beyond the tooltip: the item level, and
|
||||
/// the five spell slots whose id names a `Spell.dbc` row with their charges, in slot order.
|
||||
#[test]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! The inputs of the item tooltip's lock check (`0x5da5e0`): `Lock.dbc` pushed whole once, and what
|
||||
//! the active player opens locks with, pushed on change. The walk and the colour are the engine's
|
||||
//! ([`benilla_ui::script::check_lock`]).
|
||||
//! The inputs of the item tooltip's lock check (`0x5da5e0`): `Lock.dbc` and `LockType.dbc`'s names
|
||||
//! pushed whole once, and what the active player opens locks with, pushed on change. The walk and
|
||||
//! the colour are the engine's ([`benilla_ui::script::check_lock`]).
|
||||
|
||||
use std::collections::{BTreeSet, HashMap, HashSet};
|
||||
|
||||
@@ -11,10 +11,12 @@ use benilla_ui::script::{LockOpener, LockOpeners, LockSlotView, UiScript};
|
||||
|
||||
use crate::ui_script::gate;
|
||||
|
||||
/// Pushes every `Lock.dbc` row once per VM, empty slots dropped: the walk skips a type 0 slot.
|
||||
/// Pushes every `Lock.dbc` row and `LockType.dbc` name once per VM. Both load together; without
|
||||
/// `LockType.dbc` every lock kind reads `UNKNOWN`, as a missing row does.
|
||||
pub(super) fn feed_lock_rows(
|
||||
script: Option<NonSendMut<UiScript>>,
|
||||
locks: Option<Res<crate::go_templates::Locks>>,
|
||||
lock_types: Option<Res<crate::go_templates::LockTypes>>,
|
||||
mut pushed: Local<crate::ui_script::VmMemo<bool>>,
|
||||
) {
|
||||
let Some(mut script) = script else {
|
||||
@@ -28,18 +30,24 @@ pub(super) fn feed_lock_rows(
|
||||
};
|
||||
let rows = row_views(&locks.0);
|
||||
script.set_lock_rows(rows);
|
||||
script.set_lock_type_names(
|
||||
lock_types
|
||||
.iter()
|
||||
.flat_map(|t| t.0.names())
|
||||
.map(|(id, name)| (id, name.to_string()))
|
||||
.collect(),
|
||||
);
|
||||
*pushed.get(&script) = true;
|
||||
}
|
||||
|
||||
/// Every row as the walk reads it, empty slots dropped (a type 0 slot is skipped) and the rest in
|
||||
/// slot order.
|
||||
/// Every row as the reference holds it, all eight slots in order: the walk skips a type 0 slot,
|
||||
/// and the line under `LOCKED` reads the first slot's type whatever it is (`0x52be5d`).
|
||||
fn row_views(catalog: &LockCatalog) -> HashMap<u32, Vec<LockSlotView>> {
|
||||
catalog
|
||||
.rows()
|
||||
.map(|(id, slots)| {
|
||||
let slots = slots
|
||||
.iter()
|
||||
.filter(|s| s.key_type != benilla_formats::LOCK_KEY_NONE)
|
||||
.map(|s| LockSlotView {
|
||||
key_type: s.key_type,
|
||||
index: s.index,
|
||||
@@ -181,7 +189,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rows_empty_slots_are_dropped_and_the_rest_keep_their_order() {
|
||||
fn a_row_keeps_all_its_slots_in_place() {
|
||||
let mut slots = [LockSlot::default(); MAX_LOCK_SLOTS];
|
||||
slots[1] = LockSlot {
|
||||
key_type: 1,
|
||||
@@ -199,21 +207,28 @@ mod tests {
|
||||
(7, slots),
|
||||
(8, [LockSlot::default(); MAX_LOCK_SLOTS]),
|
||||
]));
|
||||
assert_eq!(rows[&7].len(), MAX_LOCK_SLOTS);
|
||||
assert_eq!(rows[&7][0], LockSlotView::default(), "slot 0 stays empty");
|
||||
assert_eq!(
|
||||
rows[&7],
|
||||
[
|
||||
LockSlotView {
|
||||
key_type: 1,
|
||||
index: 5060,
|
||||
skill: 0
|
||||
},
|
||||
LockSlotView {
|
||||
key_type: 2,
|
||||
index: 1,
|
||||
skill: 25
|
||||
}
|
||||
]
|
||||
rows[&7][1],
|
||||
LockSlotView {
|
||||
key_type: 1,
|
||||
index: 5060,
|
||||
skill: 0
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
rows[&7][3],
|
||||
LockSlotView {
|
||||
key_type: 2,
|
||||
index: 1,
|
||||
skill: 25
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
rows[&8].len(),
|
||||
MAX_LOCK_SLOTS,
|
||||
"a row of empty slots stays a row"
|
||||
);
|
||||
assert!(rows[&8].is_empty(), "a row of empty slots stays a row");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1229,6 +1229,7 @@ fn shipped_bag_frame_drives_end_to_end() {
|
||||
cooldown: None,
|
||||
readable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
flags: 0,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
|
||||
@@ -407,6 +407,7 @@ fn bag_tooltip_hangs_left_when_the_slot_sits_in_the_right_half() {
|
||||
cooldown: None,
|
||||
readable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
flags: 0,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
@@ -492,6 +493,7 @@ fn hovered_bag_tooltip_fills_itself_when_the_stats_land() {
|
||||
cooldown: None,
|
||||
readable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
flags: 0,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
@@ -607,6 +609,7 @@ fn vendor_bag_hover_shows_sell_price_and_arms_the_pouch_cursor() {
|
||||
cooldown: None,
|
||||
readable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
flags: 0,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
@@ -637,6 +640,7 @@ fn vendor_bag_hover_shows_sell_price_and_arms_the_pouch_cursor() {
|
||||
cooldown: None,
|
||||
readable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
flags: 0,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
@@ -832,6 +836,7 @@ fn drag_across_two_slots_queues_the_same_move_a_click_pickup_would() {
|
||||
cooldown: None,
|
||||
readable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
flags: 0,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
@@ -926,6 +931,7 @@ fn a_second_bag_window_feeds_and_paints_via_the_bag_bar() {
|
||||
cooldown: None,
|
||||
readable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
flags: 0,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
@@ -1070,6 +1076,7 @@ pub(super) fn open_backpack_with_a_five_stack(s: &mut UiScript) -> (f32, f32) {
|
||||
cooldown: None,
|
||||
readable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
flags: 0,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
@@ -1482,6 +1489,7 @@ fn the_bar_bag_buttons_name_themselves_on_hover() {
|
||||
guid: 0x70d,
|
||||
equip_slots: vec![20],
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
});
|
||||
|
||||
@@ -72,6 +72,7 @@ fn inventory_with_head_item() -> InventorySlots {
|
||||
guid: 0x70e,
|
||||
equip_slots: vec![1],
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
});
|
||||
@@ -106,6 +107,7 @@ fn backpack_with_fitting_helm() -> benilla_ui::script::ContainerState {
|
||||
cooldown: None,
|
||||
readable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
flags: 0,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
|
||||
@@ -404,6 +404,7 @@ fn chat_click_dismisses_a_stuck_spell_but_not_an_item() {
|
||||
cooldown: None,
|
||||
readable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
flags: 0,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
|
||||
@@ -36,6 +36,7 @@ fn one_item_backpack(item_id: u32, name: &str, quality: u32) -> ContainerState {
|
||||
cooldown: None,
|
||||
readable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
flags: 0,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
|
||||
@@ -34,6 +34,7 @@ fn one_item_backpack() -> ContainerState {
|
||||
cooldown: None,
|
||||
readable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
flags: 0,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
@@ -283,6 +284,7 @@ fn escape_closes_an_open_stack_split_frame() {
|
||||
cooldown: None,
|
||||
readable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
flags: 0,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
|
||||
@@ -716,6 +716,7 @@ fn backpack() -> ContainerState {
|
||||
cooldown: None,
|
||||
readable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
flags: 0,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
|
||||
@@ -84,6 +84,7 @@ fn seed_items(s: &mut UiScript) {
|
||||
guid: 0x715,
|
||||
equip_slots: vec![1],
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
});
|
||||
@@ -139,6 +140,7 @@ fn seed_items(s: &mut UiScript) {
|
||||
cooldown: None,
|
||||
readable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
flags: 0,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
@@ -214,6 +216,7 @@ fn doll_hover_renders_the_live_instance() {
|
||||
guid: 0x717,
|
||||
equip_slots: vec![1],
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
});
|
||||
|
||||
@@ -590,6 +590,7 @@ fn resolve_slot(
|
||||
item: &TradeItem,
|
||||
items: &Items,
|
||||
icons: Option<&ItemDisplays>,
|
||||
names: &NameCache,
|
||||
commands: &NetCommands,
|
||||
) -> TradeSlotItem {
|
||||
let template = items.template(item.entry, 0, commands);
|
||||
@@ -610,6 +611,12 @@ fn resolve_slot(
|
||||
(Some(n), Some(q)) => Some(crate::ui_items::item_link(item.entry, n, q)),
|
||||
_ => None,
|
||||
};
|
||||
// Only a wrapped gift asks for its giver, and a zero guid asks nothing (`0x52e288`,
|
||||
// `0x52e29d`).
|
||||
let gift_creator = (item.wrapped && item.gift_creator != 0)
|
||||
.then(|| names.resolve_player(item.gift_creator, commands))
|
||||
.flatten()
|
||||
.map(str::to_string);
|
||||
TradeSlotItem {
|
||||
item_id: item.entry,
|
||||
name,
|
||||
@@ -619,6 +626,8 @@ fn resolve_slot(
|
||||
inventory_type,
|
||||
lock_id: item.lock_id,
|
||||
object_flags: None,
|
||||
wrapped: item.wrapped,
|
||||
gift_creator,
|
||||
enchantment: None,
|
||||
link,
|
||||
}
|
||||
@@ -652,6 +661,9 @@ fn own_item(
|
||||
let fields = objects.object(guid)?;
|
||||
let entry = fields.object_entry()?;
|
||||
let flags = fields.item_flags().unwrap_or(0);
|
||||
// The placed item's WRAPPED flag and giver, which its slot's tooltip reads off the object
|
||||
// (`0x52b789`, `0x52e32a`).
|
||||
let wrapped = flags & benilla_protocol::messages::ITEM_DYNFLAG_WRAPPED != 0;
|
||||
let display_id = items
|
||||
.template(entry, guid, commands)
|
||||
.map(|t| t.display_info_id)
|
||||
@@ -661,8 +673,8 @@ fn own_item(
|
||||
entry,
|
||||
display_id,
|
||||
count,
|
||||
wrapped: false,
|
||||
gift_creator: 0,
|
||||
wrapped,
|
||||
gift_creator: fields.item_gift_creator().unwrap_or(0),
|
||||
perm_enchant: 0,
|
||||
creator: 0,
|
||||
charges: 0,
|
||||
@@ -687,12 +699,13 @@ fn resolve_side(
|
||||
enchant: Option<&str>,
|
||||
items: &Items,
|
||||
icons: Option<&ItemDisplays>,
|
||||
names: &NameCache,
|
||||
commands: &NetCommands,
|
||||
) -> TradeSideState {
|
||||
let mut slots: [Option<TradeSlotItem>; TRADE_SLOT_COUNT] = Default::default();
|
||||
for (i, s) in offer.slots.iter().enumerate() {
|
||||
slots[i] = s.map(|it| {
|
||||
let mut row = resolve_slot(&it, items, icons, commands);
|
||||
let mut row = resolve_slot(&it, items, icons, names, commands);
|
||||
// Our items are live objects; the partner's are blocks (`0x5342ed`, `0x53456b`).
|
||||
row.object_flags = ours.then_some(offer.object_flags[i]);
|
||||
if i == ENCHANT_SLOT {
|
||||
@@ -712,6 +725,7 @@ fn snapshot(
|
||||
items: &Items,
|
||||
icons: Option<&ItemDisplays>,
|
||||
spells: Option<&crate::ui_action::Spells>,
|
||||
names: &NameCache,
|
||||
commands: &NetCommands,
|
||||
) -> Option<TradeState> {
|
||||
if !trade.open {
|
||||
@@ -732,6 +746,7 @@ fn snapshot(
|
||||
name(trade.their.enchant_spell_id),
|
||||
items,
|
||||
icons,
|
||||
names,
|
||||
commands,
|
||||
),
|
||||
// Our window carries the spell we cast on theirs (`0x4c02b7`, side 0).
|
||||
@@ -741,6 +756,7 @@ fn snapshot(
|
||||
name(trade.our.enchant_spell_id),
|
||||
items,
|
||||
icons,
|
||||
names,
|
||||
commands,
|
||||
),
|
||||
})
|
||||
@@ -789,6 +805,7 @@ fn resignal_trade_items(
|
||||
objects: Objects,
|
||||
items: Res<Items>,
|
||||
icons: Option<Res<ItemDisplays>>,
|
||||
names: Res<NameCache>,
|
||||
) {
|
||||
use crate::item_watch::TradeTouch;
|
||||
let touches: Vec<TradeTouch> = cursor
|
||||
@@ -839,7 +856,7 @@ fn resignal_trade_items(
|
||||
slot: wire_slot,
|
||||
},
|
||||
);
|
||||
let mut row = resolve_slot(&item, &items, icons.as_deref(), &commands);
|
||||
let mut row = resolve_slot(&item, &items, icons.as_deref(), &names, &commands);
|
||||
row.object_flags = Some(flags);
|
||||
script.replace_trade_slot(slot, row);
|
||||
}
|
||||
@@ -889,6 +906,7 @@ fn feed_trade(
|
||||
&items,
|
||||
icons.as_deref(),
|
||||
spells.as_deref(),
|
||||
&names,
|
||||
&commands,
|
||||
);
|
||||
// A close is the core's, which shut the VM's window and left both offers in place; a
|
||||
@@ -1249,7 +1267,13 @@ mod tests {
|
||||
items.insert_template(1234, Some(sword));
|
||||
items.insert_template(1235, Some(crate::items::test_template("Linen Cloth")));
|
||||
|
||||
let row = resolve_slot(&wire_item(1234), &items, None, &commands);
|
||||
let row = resolve_slot(
|
||||
&wire_item(1234),
|
||||
&items,
|
||||
None,
|
||||
&NameCache::default(),
|
||||
&commands,
|
||||
);
|
||||
assert_eq!((row.quality, row.inventory_type), (Some(4), 13));
|
||||
// The icon is the template's display's, the question mark when that row is missing, and
|
||||
// left to the getter while the template is unanswered.
|
||||
@@ -1258,15 +1282,85 @@ mod tests {
|
||||
Some("Interface\\Icons\\INV_Misc_QuestionMark")
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_slot(&wire_item(1236), &items, None, &commands).texture,
|
||||
resolve_slot(
|
||||
&wire_item(1236),
|
||||
&items,
|
||||
None,
|
||||
&NameCache::default(),
|
||||
&commands
|
||||
)
|
||||
.texture,
|
||||
None
|
||||
);
|
||||
let row = resolve_slot(&wire_item(1235), &items, None, &commands);
|
||||
let row = resolve_slot(
|
||||
&wire_item(1235),
|
||||
&items,
|
||||
None,
|
||||
&NameCache::default(),
|
||||
&commands,
|
||||
);
|
||||
assert_eq!((row.quality, row.inventory_type), (Some(1), 0));
|
||||
let row = resolve_slot(&wire_item(1236), &items, None, &commands);
|
||||
let row = resolve_slot(
|
||||
&wire_item(1236),
|
||||
&items,
|
||||
None,
|
||||
&NameCache::default(),
|
||||
&commands,
|
||||
);
|
||||
assert_eq!((row.quality, row.inventory_type), (None, 0));
|
||||
}
|
||||
|
||||
/// A wrapped gift's giver rides both columns: the partner's block carries the wire's wrapped
|
||||
/// word and gift creator (`0x534559`, `0x534574`), our placed item's object the same; only a
|
||||
/// wrapped item asks for the name, a zero guid asks nothing, and a name in flight is `None`.
|
||||
#[test]
|
||||
fn a_wrapped_gifts_giver_reaches_both_columns() {
|
||||
let (tx, rx) = crossbeam_channel::unbounded();
|
||||
let commands = NetCommands(tx);
|
||||
let mut items = Items::default();
|
||||
items.insert_template(1234, Some(crate::items::test_template("Gift")));
|
||||
let mut names = NameCache::default();
|
||||
names.insert_player(0x7, "Santa".to_string(), None);
|
||||
let gift = |wrapped, giver| TradeItem {
|
||||
wrapped,
|
||||
gift_creator: giver,
|
||||
..wire_item(1234)
|
||||
};
|
||||
let mut s = TradeSession::default();
|
||||
s.begin(0x9);
|
||||
s.open_window();
|
||||
s.place_own_item(1, gift(true, 0x7), 0x8);
|
||||
s.place_own_item(2, gift(false, 0x7), 0);
|
||||
s.place_own_item(3, gift(true, 0), 0x8);
|
||||
s.place_own_item(4, gift(true, 0x8), 0x8);
|
||||
s.their.slots[0] = Some(gift(true, 0x7));
|
||||
s.their.slots[1] = Some(gift(false, 0x7));
|
||||
let state = snapshot(&s, &items, None, None, &names, &commands).expect("open");
|
||||
let giver = |side: &TradeSideState, i: usize| {
|
||||
let row = side.slots[i].as_ref().expect("filled");
|
||||
(row.wrapped, row.gift_creator.clone())
|
||||
};
|
||||
assert_eq!(giver(&state.target, 0), (true, Some("Santa".into())));
|
||||
assert_eq!(
|
||||
giver(&state.target, 1),
|
||||
(false, None),
|
||||
"unwrapped asks nothing"
|
||||
);
|
||||
assert_eq!(giver(&state.player, 0), (true, Some("Santa".into())));
|
||||
assert_eq!(giver(&state.player, 1), (false, None));
|
||||
assert_eq!(
|
||||
giver(&state.player, 2),
|
||||
(true, None),
|
||||
"a zero guid names no one"
|
||||
);
|
||||
assert_eq!(giver(&state.player, 3), (true, None), "a name in flight");
|
||||
let asked: Vec<_> = rx.try_iter().collect();
|
||||
assert!(
|
||||
matches!(asked.as_slice(), [ClientCommand::NameQuery { guid: 0x8 }]),
|
||||
"only the wrapped, nonzero, uncached giver is asked: {asked:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_close_state_machine() {
|
||||
let mut s = TradeSession::default();
|
||||
@@ -2900,6 +2994,48 @@ mod tests {
|
||||
assert!(seen(&mut app).is_empty());
|
||||
}
|
||||
|
||||
/// The placed item is a live object: its WRAPPED flag and `ITEM_FIELD_GIFTCREATOR` reach the
|
||||
/// slot when the watchers place it again (`0x52b789`, `0x52e32a`), the giver by name.
|
||||
#[test]
|
||||
fn a_wrapped_offer_placed_again_carries_its_giver() {
|
||||
use benilla_protocol::field::FIELD_ITEM_STACK_COUNT;
|
||||
use benilla_protocol::ObjectFields;
|
||||
let (mut app, _rx) = resignal_app();
|
||||
app.world_mut()
|
||||
.resource_mut::<NameCache>()
|
||||
.insert_player(0x77, "Santa".to_string(), None);
|
||||
let linen = app.world().resource::<GuidIndex>().0[&LINEN];
|
||||
// ITEM_FIELD_GIFTCREATOR is the guid at fields 12 and 13, ITEM_FIELD_FLAGS field 21.
|
||||
app.world_mut()
|
||||
.get_mut::<ObjectStore>(linen)
|
||||
.unwrap()
|
||||
.0
|
||||
.merge(ObjectFields::from_pairs(&[
|
||||
(FIELD_ITEM_STACK_COUNT, 2),
|
||||
(12, 0x77),
|
||||
(13, 0),
|
||||
(21, 0x8),
|
||||
]));
|
||||
notify(
|
||||
&mut app,
|
||||
crate::item_watch::ItemNotice::StackCount {
|
||||
guid: LINEN,
|
||||
entry: 2589,
|
||||
},
|
||||
);
|
||||
let slots = app
|
||||
.world()
|
||||
.non_send_resource::<UiScript>()
|
||||
.trade_player_slots()
|
||||
.expect("open trade");
|
||||
let row = slots[0].as_ref().expect("the slot is filled");
|
||||
assert_eq!(row.object_flags, Some(0x8));
|
||||
assert_eq!(
|
||||
(row.wrapped, row.gift_creator.as_deref()),
|
||||
(true, Some("Santa"))
|
||||
);
|
||||
}
|
||||
|
||||
/// `0x4bfb60` from the stack and enchantment watchers: the same item placed again from its
|
||||
/// position, the slot showing the live count, and one signal; a placement of an item that
|
||||
/// is no longer an object sends and signals nothing (`0x5d43da`).
|
||||
|
||||
@@ -43,6 +43,8 @@ fn item(item_id: u32, name: &str, count: u32, quality: u32) -> TradeSlotItem {
|
||||
inventory_type: 0,
|
||||
lock_id: 0,
|
||||
object_flags: None,
|
||||
wrapped: false,
|
||||
gift_creator: None,
|
||||
enchantment: None,
|
||||
link: Some(format!("|cffffffff|Hitem:{item_id}:0:0:0|h[{name}]|h|r")),
|
||||
}
|
||||
|
||||
@@ -62,13 +62,23 @@ pub(crate) fn str_at(rs: &RecordSet, r: &Record, i: usize) -> Option<String> {
|
||||
/// The column's raw dword is the offset, as the client reads it, whatever the schema typed it: the
|
||||
/// other locales' columns are often left untyped.
|
||||
pub(crate) fn loc_str(rs: &RecordSet, r: &Record, first: usize, locale: Locale) -> Option<String> {
|
||||
loc_str_raw(rs, r, first, locale).filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
/// [`loc_str`] for a reader that tells an empty name from no column: `Some("")` is a name the
|
||||
/// file leaves blank, `None` a column or offset that cannot be read.
|
||||
pub(crate) fn loc_str_raw(
|
||||
rs: &RecordSet,
|
||||
r: &Record,
|
||||
first: usize,
|
||||
locale: Locale,
|
||||
) -> Option<String> {
|
||||
let off = match r.get_value(first + locale.index())? {
|
||||
Value::StringRef(StringRef(off)) | Value::UInt32(off) => *off,
|
||||
Value::Int32(off) => *off as u32,
|
||||
Value::Float32(f) => f.to_bits(),
|
||||
};
|
||||
let s = rs.get_string(StringRef(off)).ok()?;
|
||||
(!s.is_empty()).then(|| s.to_string())
|
||||
rs.get_string(StringRef(off)).ok().map(|s| s.to_string())
|
||||
}
|
||||
|
||||
/// `SpellIcon.dbc`: id → extensionless texture path (`Interface\Icons\…`).
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::Chain;
|
||||
use anyhow::{Context, Result};
|
||||
use benilla_dbc::{FieldType, Schema, SchemaField};
|
||||
|
||||
use crate::dbc::{loc_str, parse, str_at, u32_at};
|
||||
use crate::dbc::{loc_str_raw, parse, str_at, u32_at};
|
||||
|
||||
const LOCK_TYPE: &str = "DBFilesClient\\LockType.dbc";
|
||||
/// The file's column count, which `benilla-dbc` checks against the header.
|
||||
@@ -34,9 +34,19 @@ impl LockTypeCatalog {
|
||||
}
|
||||
|
||||
/// A lock kind's `Name` ("Herbalism"), filled into "Requires %s" (`0x5f34f9`); the caller
|
||||
/// supplies the reference's `"UNKNOWN"` for a missing row.
|
||||
/// supplies the reference's `"UNKNOWN"` for a missing row, and a blank name is none here.
|
||||
pub fn name(&self, lock_type_id: u32) -> Option<&str> {
|
||||
self.names.get(&lock_type_id).map(String::as_str)
|
||||
self.names
|
||||
.get(&lock_type_id)
|
||||
.map(String::as_str)
|
||||
.filter(|n| !n.is_empty())
|
||||
}
|
||||
|
||||
/// Every row's `Name`, by `LockType` index, a blank one included: the item tooltip answers
|
||||
/// `UNKNOWN` only for a row that is missing (`0x52bede`..`0x52bef2`), else the name as it is,
|
||||
/// blank too (`0x52bef4`).
|
||||
pub fn names(&self) -> impl Iterator<Item = (u32, &str)> {
|
||||
self.names.iter().map(|(&id, name)| (id, name.as_str()))
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
@@ -67,8 +77,11 @@ pub fn load_lock_type_catalog(chain: &mut Chain) -> Result<LockTypeCatalog> {
|
||||
let bytes = chain
|
||||
.read_file(LOCK_TYPE)
|
||||
.with_context(|| format!("reading {LOCK_TYPE}"))?;
|
||||
let rs = parse(&bytes, schema(), "LockType.dbc")?;
|
||||
let locale = chain.locale();
|
||||
catalog_from_bytes(&bytes, chain.locale())
|
||||
}
|
||||
|
||||
fn catalog_from_bytes(bytes: &[u8], locale: crate::Locale) -> Result<LockTypeCatalog> {
|
||||
let rs = parse(bytes, schema(), "LockType.dbc")?;
|
||||
let mut cursors = HashMap::new();
|
||||
let mut names = HashMap::new();
|
||||
for r in rs.records() {
|
||||
@@ -76,7 +89,7 @@ pub fn load_lock_type_catalog(chain: &mut Chain) -> Result<LockTypeCatalog> {
|
||||
if let Some(name) = str_at(&rs, r, CURSOR_NAME_FIELD) {
|
||||
cursors.insert(id, name);
|
||||
}
|
||||
if let Some(name) = loc_str(&rs, r, NAME_FIELD, locale) {
|
||||
if let Some(name) = loc_str_raw(&rs, r, NAME_FIELD, locale) {
|
||||
names.insert(id, name);
|
||||
}
|
||||
}
|
||||
@@ -87,6 +100,45 @@ pub fn load_lock_type_catalog(chain: &mut Chain) -> Result<LockTypeCatalog> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Two WDBC rows of the shipped shape: type 1 named, type 2 with a blank name column.
|
||||
fn synthetic() -> Vec<u8> {
|
||||
let strings = b"\0Pick Lock\0".to_vec();
|
||||
let mut bytes = b"WDBC".to_vec();
|
||||
for w in [
|
||||
2,
|
||||
LOCK_TYPE_FIELDS as u32,
|
||||
LOCK_TYPE_FIELDS as u32 * 4,
|
||||
strings.len() as u32,
|
||||
] {
|
||||
bytes.extend_from_slice(&w.to_le_bytes());
|
||||
}
|
||||
for (id, name_at) in [(1u32, 1u32), (2, 0)] {
|
||||
let mut row = vec![0u32; LOCK_TYPE_FIELDS];
|
||||
row[0] = id;
|
||||
row[NAME_FIELD] = name_at;
|
||||
for f in row {
|
||||
bytes.extend_from_slice(&f.to_le_bytes());
|
||||
}
|
||||
}
|
||||
bytes.extend_from_slice(&strings);
|
||||
bytes
|
||||
}
|
||||
|
||||
/// A blank `Name` is a row with no word, not a missing row: the item tooltip answers `UNKNOWN`
|
||||
/// only for a missing row (`0x52bede`..`0x52bef2`) and the empty string for a blank one
|
||||
/// (`0x52bef4`), so [`LockTypeCatalog::names`] lists it; the toast's [`LockTypeCatalog::name`]
|
||||
/// still reads it as none.
|
||||
#[test]
|
||||
fn a_blank_name_is_a_row_not_a_missing_one() {
|
||||
let locale = crate::Locale::from_index(0).unwrap();
|
||||
let cat = catalog_from_bytes(&synthetic(), locale).unwrap();
|
||||
let mut names: Vec<_> = cat.names().collect();
|
||||
names.sort();
|
||||
assert_eq!(names, vec![(1, "Pick Lock"), (2, "")]);
|
||||
assert_eq!(cat.name(1), Some("Pick Lock"));
|
||||
assert_eq!(cat.name(2), None);
|
||||
}
|
||||
|
||||
/// The 5875 rows: a column slip lands on another column and fails.
|
||||
#[test]
|
||||
fn real_lock_types_name_their_cursors() {
|
||||
|
||||
@@ -36,6 +36,11 @@ impl ObjectFields {
|
||||
pub fn item_creator(&self) -> Option<u64> {
|
||||
self.get_guid(10).filter(|&g| g != 0)
|
||||
}
|
||||
/// `ITEM_FIELD_GIFTCREATOR` (field 12, `[item+0x114]+0x18` at `0x52e32a`): who wrapped the
|
||||
/// item, read only while it carries the WRAPPED flag.
|
||||
pub fn item_gift_creator(&self) -> Option<u64> {
|
||||
self.get_guid(12).filter(|&g| g != 0)
|
||||
}
|
||||
/// `ITEM_FIELD_ITEM_TEXT_ID` (field 45, owner only): a mailed letter copy's text; nonzero
|
||||
/// makes a right-click read it (`CMSG_ITEM_TEXT_QUERY`) instead of using the item.
|
||||
pub fn item_text_id(&self) -> Option<u32> {
|
||||
|
||||
@@ -213,6 +213,8 @@ pub struct InvSlotView {
|
||||
pub equip_slots: Vec<u8>,
|
||||
/// The resolved `ITEM_FIELD_CREATOR` name, the tooltip's "<Made by %s>" line.
|
||||
pub creator: Option<String>,
|
||||
/// The resolved `ITEM_FIELD_GIFTCREATOR` name, the tooltip's `ITEM_WRAPPED_BY` line.
|
||||
pub gift_creator: Option<String>,
|
||||
/// The resolved enchant slots in slot order. An inspected player's record has all 7 too, but
|
||||
/// vmangos fills only PERM and TEMP (`Player.cpp:10594-10595`).
|
||||
pub enchants: Vec<super::EnchantView>,
|
||||
@@ -538,6 +540,7 @@ impl InvSlotView {
|
||||
link: slot.link.clone(),
|
||||
guid: slot.guid,
|
||||
creator: slot.creator.clone(),
|
||||
gift_creator: slot.gift_creator.clone(),
|
||||
enchants: slot.enchants.clone(),
|
||||
// The item's own flags (UNLOCKED, WRAPPED) and expiry, which the tooltip builder reads.
|
||||
flags: slot.flags,
|
||||
|
||||
@@ -72,6 +72,9 @@ pub struct ContainerSlot {
|
||||
pub readable: bool,
|
||||
/// The `ITEM_FIELD_CREATOR` name for the "Written by" or "<Made by>" line, once resolved.
|
||||
pub creator: Option<String>,
|
||||
/// The `ITEM_FIELD_GIFTCREATOR` name for a wrapped item's `ITEM_WRAPPED_BY` line, once
|
||||
/// resolved; only a wrapped item asks for it, as the gift arm does (`0x52e29d`).
|
||||
pub gift_creator: Option<String>,
|
||||
/// `ITEM_FIELD_FLAGS`; the tooltip reads UNLOCKED `0x4` and WRAPPED `0x8`.
|
||||
pub flags: u32,
|
||||
/// Bound at runtime (`0x5da2c0`): `ITEM_FIELD_FLAGS & 1`, or an enchant whose
|
||||
@@ -1059,6 +1062,7 @@ mod tests {
|
||||
spell_0: false,
|
||||
readable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
flags: 0,
|
||||
enchants: Vec::new(),
|
||||
},
|
||||
@@ -1757,6 +1761,7 @@ mod tests {
|
||||
spell_0: false,
|
||||
readable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
flags: 0,
|
||||
enchants: Vec::new(),
|
||||
},
|
||||
|
||||
@@ -1030,6 +1030,7 @@ mod tests {
|
||||
cooldown: None,
|
||||
readable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
flags: 0,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
|
||||
@@ -375,6 +375,7 @@ mod tests {
|
||||
guid: 0x10c,
|
||||
equip_slots: vec![1],
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
});
|
||||
@@ -399,6 +400,7 @@ mod tests {
|
||||
guid: 0x10d,
|
||||
equip_slots: vec![11, 12], // either finger: the doll-to-doll swap
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
});
|
||||
@@ -423,6 +425,7 @@ mod tests {
|
||||
guid: 0x10e,
|
||||
equip_slots: vec![19],
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
});
|
||||
@@ -456,6 +459,7 @@ mod tests {
|
||||
cooldown: None,
|
||||
readable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
flags: 0,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
@@ -615,6 +619,7 @@ mod tests {
|
||||
guid: 0x110,
|
||||
equip_slots: vec![20, 21, 22, 23],
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
});
|
||||
@@ -660,6 +665,7 @@ mod tests {
|
||||
cooldown: None,
|
||||
readable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
flags: 0,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
//! The item tooltip's lock check, the reference's `0x5da5e0`: whether the active player can open
|
||||
//! an item's lock, and by what. It colours the `LOCKED` line (`0x52be0c`..`0x52be3d`).
|
||||
//! an item's lock, and by what. It colours the `LOCKED` line (`0x52be0c`..`0x52be3d`) and decides
|
||||
//! the `LOCKED_WITH_SPELL_KNOWN` line under it (`0x52be98`).
|
||||
//!
|
||||
//! The engine reads no DBCs, so the app pushes the `Lock.dbc` rows ([`LockSlotView`]) once and the
|
||||
//! player's means of opening ([`LockOpeners`]) on change; the walk is here.
|
||||
//! The engine reads no DBCs, so the app pushes the `Lock.dbc` rows ([`LockSlotView`]) once, the
|
||||
//! `LockType.dbc` names once and the player's means of opening ([`LockOpeners`]) on change; the
|
||||
//! walk is here.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
@@ -10,7 +12,8 @@ use super::UiScript;
|
||||
|
||||
/// One `Lock.dbc` slot as `0x5da5e0` reads it: the key type at `lock+0x4`, the index at `+0x24` and
|
||||
/// the skill at `+0x44`, each 8 wide. Its action column (`+0x64`) is not read: unlike the game
|
||||
/// object walk (`0x5f83d0`), an item's lock has no state to gate a slot on.
|
||||
/// object walk (`0x5f83d0`), an item's lock has no state to gate a slot on. A row keeps its empty
|
||||
/// slots, since the sub-line under `LOCKED` reads the first slot's type whatever it is (`0x52be5d`).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct LockSlotView {
|
||||
/// 0 empty, 1 a key item, 2 a skill.
|
||||
@@ -54,60 +57,115 @@ pub enum LockCheck {
|
||||
Open,
|
||||
}
|
||||
|
||||
/// What `0x5da5e0` reports through its out parameters and return value.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub(in crate::script) struct LockWalk {
|
||||
/// The return value is nonzero (`0x5da64e`, `0x5da725`): the lock has a key or skill slot and
|
||||
/// none opened it.
|
||||
pub closed: bool,
|
||||
/// A held key opened it: the walk set the item out parameter (`0x5da78f`).
|
||||
pub key_held: bool,
|
||||
/// The spell out parameters (`*a1` nonzero, `*a2` points, `*a3` required, `0x5da6cc`..
|
||||
/// `0x5da6f4`) of the last known open-lock spell the walk tried on a skill slot of its lock
|
||||
/// type, whether or not its skill reached the requirement. A held key overwrites `*a1` with
|
||||
/// its use spell (`0x5da781`; `0x5d8c80` returns one when the key has one), so this clears:
|
||||
/// the colour call ignores `*a1` once `*a4` is set (`0x52be1c`), and no `Lock.dbc` row has a
|
||||
/// key slot after a skill slot, so the second walk never loses a spell to a key.
|
||||
pub spell: Option<SpellTry>,
|
||||
}
|
||||
|
||||
/// One open-lock spell tried against one skill slot.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(in crate::script) struct SpellTry {
|
||||
/// The slot's requirement.
|
||||
pub required: i32,
|
||||
/// The spell's skill for the player.
|
||||
pub points: i32,
|
||||
}
|
||||
|
||||
/// `0x5da5e0(lock, level)`. `level` is the item record's `+0x38`, the fallback behind a slot whose
|
||||
/// skill is 0. `openers` is `None` with no active player, which opens everything.
|
||||
/// skill is 0. `openers` is `None` with no active player, which opens everything and tells
|
||||
/// nothing (`0x5da60b`).
|
||||
///
|
||||
/// Walks the slots in order: a skill slot (type 2) scans the known spells for an open-lock effect
|
||||
/// whose misc value is its index and whose points reach its requirement (`0x5da6f8 jge`); a key
|
||||
/// slot (type 1) looks for the item. The first to succeed ends the walk. A slot of any other type
|
||||
/// is skipped. A type 1 or 2 slot makes the lock closed unless one succeeds (`0x5da64e`,
|
||||
/// `0x5da725`).
|
||||
pub fn check_lock(slots: &[LockSlotView], level: u32, openers: Option<&LockOpeners>) -> LockCheck {
|
||||
pub(in crate::script) fn walk_lock(
|
||||
slots: &[LockSlotView],
|
||||
level: u32,
|
||||
openers: Option<&LockOpeners>,
|
||||
) -> LockWalk {
|
||||
let Some(openers) = openers else {
|
||||
return LockCheck::Open;
|
||||
return LockWalk::default();
|
||||
};
|
||||
let mut closed = false;
|
||||
let mut walk = LockWalk::default();
|
||||
for slot in slots {
|
||||
match slot.key_type {
|
||||
2 => {
|
||||
closed = true;
|
||||
walk.closed = true;
|
||||
let required = if slot.skill != 0 {
|
||||
slot.skill as i32
|
||||
} else {
|
||||
level.wrapping_mul(5) as i32
|
||||
};
|
||||
for spell in &openers.spells {
|
||||
if spell.lock_type == slot.index && spell.points >= required {
|
||||
return LockCheck::Skill {
|
||||
if spell.lock_type == slot.index {
|
||||
walk.spell = Some(SpellTry {
|
||||
required,
|
||||
points: spell.points,
|
||||
};
|
||||
});
|
||||
if spell.points >= required {
|
||||
walk.closed = false;
|
||||
return walk;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
closed = true;
|
||||
walk.closed = true;
|
||||
if openers.held.contains(&slot.index) {
|
||||
return LockCheck::Open;
|
||||
return LockWalk {
|
||||
closed: false,
|
||||
key_held: true,
|
||||
spell: None,
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if closed {
|
||||
LockCheck::Closed
|
||||
} else {
|
||||
LockCheck::Open
|
||||
walk
|
||||
}
|
||||
|
||||
/// `0x5da5e0` read the way the `LOCKED` line's colour reads it (`0x52be11`..`0x52be3d`).
|
||||
pub fn check_lock(slots: &[LockSlotView], level: u32, openers: Option<&LockOpeners>) -> LockCheck {
|
||||
let walk = walk_lock(slots, level, openers);
|
||||
match (walk.closed, walk.key_held, walk.spell) {
|
||||
(true, _, _) => LockCheck::Closed,
|
||||
(false, false, Some(SpellTry { required, points })) => {
|
||||
LockCheck::Skill { required, points }
|
||||
}
|
||||
_ => LockCheck::Open,
|
||||
}
|
||||
}
|
||||
|
||||
impl UiScript {
|
||||
/// Pushes `Lock.dbc` whole, by lock id, once at load: a clicked tooltip cannot repaint on a late
|
||||
/// answer. The reference reads its loaded store (`0xc0dae4`).
|
||||
/// answer. The reference reads its loaded store (`0xc0dae4`), and a lock id without a row is no
|
||||
/// lock to the item builder (`0x52bd74`).
|
||||
pub fn set_lock_rows(&mut self, rows: std::collections::HashMap<u32, Vec<LockSlotView>>) {
|
||||
self.model_mut().lock_rows = rows;
|
||||
}
|
||||
|
||||
/// Pushes `LockType.dbc`'s names, by index, once at load: the `LOCKED_WITH_SPELL_KNOWN` line
|
||||
/// names the lock kind off the store `0xc0dad0` (`0x52bef4`); an index without a row reads
|
||||
/// `UNKNOWN` (`0x52bf00`).
|
||||
pub fn set_lock_type_names(&mut self, names: std::collections::HashMap<u32, String>) {
|
||||
self.model_mut().lock_type_names = names;
|
||||
}
|
||||
|
||||
/// Pushes what the active player opens locks with, on change. Never pushed, there is no active
|
||||
/// player and every lock reads as open.
|
||||
pub fn set_lock_openers(&mut self, openers: LockOpeners) {
|
||||
|
||||
@@ -993,9 +993,11 @@ pub(crate) struct Model {
|
||||
pub(crate) player_req: PlayerReqState,
|
||||
/// `Lock.dbc` by lock id, pushed whole at startup, and what the active player opens locks with
|
||||
/// (`None` before the first push: no active player). The item tooltip's `LOCKED` colour walks
|
||||
/// them ([`super::lock_check::check_lock`]).
|
||||
/// them ([`super::lock_check::check_lock`]); `LockType.dbc`'s names by index name its
|
||||
/// `LOCKED_WITH_SPELL_KNOWN` line.
|
||||
pub(crate) lock_rows: HashMap<u32, Vec<super::LockSlotView>>,
|
||||
pub(crate) lock_openers: Option<super::LockOpeners>,
|
||||
pub(crate) lock_type_names: HashMap<u32, String>,
|
||||
/// Spell id to its tooltip view, and the misses asked for.
|
||||
pub(crate) spell_tooltips: HashMap<u32, super::SpellTooltipView>,
|
||||
pub(crate) spell_tooltip_asks: HashSet<u32>,
|
||||
@@ -1684,6 +1686,7 @@ impl Model {
|
||||
player_req: PlayerReqState::default(),
|
||||
lock_rows: HashMap::new(),
|
||||
lock_openers: None,
|
||||
lock_type_names: HashMap::new(),
|
||||
spell_tooltips: HashMap::new(),
|
||||
spell_tooltip_asks: HashSet::new(),
|
||||
pet_spell_tooltips: HashMap::new(),
|
||||
|
||||
@@ -86,6 +86,7 @@ fn merchant_compare_renders_the_compare_shape_and_no_hover_seats_a_plate() {
|
||||
cooldown: None,
|
||||
readable: false,
|
||||
creator: None,
|
||||
gift_creator: None,
|
||||
flags: 0,
|
||||
enchants: Vec::new(),
|
||||
spell_0: false,
|
||||
@@ -223,6 +224,7 @@ fn set_inventory_item_name_only_is_the_trimmed_build() {
|
||||
..Default::default()
|
||||
});
|
||||
s.set_inventory_slots(inv);
|
||||
s.set_lock_rows(HashMap::from([(7, Vec::new())]));
|
||||
s.set_item_template(
|
||||
8100,
|
||||
ItemTemplateView {
|
||||
@@ -790,6 +792,7 @@ fn locked_is_drawn_for_a_locked_object_and_sets_the_builder_flag() {
|
||||
slots: HashMap::from([(1, slot(0)), (2, slot(0x4))]),
|
||||
}),
|
||||
);
|
||||
s.set_lock_rows(HashMap::from([(7, Vec::new())]));
|
||||
s.set_item_template(
|
||||
8300,
|
||||
ItemTemplateView {
|
||||
@@ -854,6 +857,7 @@ fn bank_vault_items_keep_their_flags_and_expiry() {
|
||||
]),
|
||||
}),
|
||||
);
|
||||
s.set_lock_rows(HashMap::from([(7, Vec::new())]));
|
||||
s.set_item_template(
|
||||
8300,
|
||||
ItemTemplateView {
|
||||
|
||||
@@ -580,6 +580,7 @@ fn instance_tail_creator_and_readable() {
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
s.set_lock_rows(HashMap::from([(7, Vec::new())]));
|
||||
s.set_item_template(
|
||||
2589,
|
||||
ItemTemplateView {
|
||||
@@ -690,6 +691,187 @@ fn instance_tail_creator_and_readable() {
|
||||
assert!(s.take_errors().is_empty());
|
||||
}
|
||||
|
||||
/// A wrapped item (`ITEM_FIELD_FLAGS & 0x8`, `0x52b789`) takes the gift arm (`0x52e1b9`): it names
|
||||
/// its giver off `ITEM_FIELD_GIFTCREATOR` in white (`0x52e2cb`) and never prints the creator's
|
||||
/// line. A giver not yet resolved adds nothing, and an unwrapped item ignores the field.
|
||||
#[test]
|
||||
fn a_wrapped_item_names_its_giver_and_hides_the_creator_line() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(r#"ITEM_WRAPPED_BY = "[WRAPPED_BY %s]""#).unwrap();
|
||||
s.set_item_template(
|
||||
5042,
|
||||
ItemTemplateView {
|
||||
name: "Gift".into(),
|
||||
quality: 1,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let slot = |flags: u32, creator: Option<&str>, gift: Option<&str>| ContainerSlot {
|
||||
item_id: 5042,
|
||||
count: 1,
|
||||
quality: Some(1),
|
||||
creator: creator.map(str::to_string),
|
||||
gift_creator: gift.map(str::to_string),
|
||||
flags,
|
||||
..Default::default()
|
||||
};
|
||||
let mut slots = HashMap::new();
|
||||
slots.insert(1, slot(0x8, Some("Geoffrey"), Some("Santa"))); // wrapped, giver known
|
||||
slots.insert(2, slot(0x8, Some("Geoffrey"), None)); // wrapped, giver zero or in flight
|
||||
slots.insert(3, slot(0, Some("Geoffrey"), Some("Santa"))); // unwrapped keeps its creator
|
||||
s.set_container(
|
||||
0,
|
||||
Some(ContainerState {
|
||||
name: Some("Backpack".into()),
|
||||
num_slots: 3,
|
||||
slots,
|
||||
}),
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "SlotG"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
CreateFrame("GameTooltip", "TT", nil, "TestTooltipTemplate")
|
||||
TT:SetOwner(a, "ANCHOR_RIGHT"); TT:SetBagItem(0, 1)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let lines = lines_of(&mut s);
|
||||
let texts: Vec<&str> = lines.iter().map(|(t, _)| t.as_str()).collect();
|
||||
assert_eq!(texts, vec!["Gift", "[WRAPPED_BY Santa]"]);
|
||||
assert_eq!(lines[1].1, [1.0; 4], "ITEM_WRAPPED_BY is white");
|
||||
for (n, want) in [
|
||||
(2, vec!["Gift"]),
|
||||
(3, vec!["Gift", "|cff00ff00[CREATED_BY Geoffrey]|r"]),
|
||||
] {
|
||||
s.run(&format!(
|
||||
r#"TT:SetOwner(getglobal("SlotG"), "ANCHOR_RIGHT"); TT:SetBagItem(0, {n})"#
|
||||
))
|
||||
.unwrap();
|
||||
let texts: Vec<String> = lines_of(&mut s).into_iter().map(|(t, _)| t).collect();
|
||||
assert_eq!(texts, want, "slot {n}");
|
||||
}
|
||||
|
||||
// A worn wrapped item reaches the same arm through the doll's view.
|
||||
let mut inv: InventorySlots = Default::default();
|
||||
inv[16] = Some(InvSlotView {
|
||||
item_id: 5042,
|
||||
name: Some("Gift".into()),
|
||||
quality: 1,
|
||||
flags: 0x8,
|
||||
creator: Some("Geoffrey".into()),
|
||||
gift_creator: Some("Santa".into()),
|
||||
..Default::default()
|
||||
});
|
||||
s.set_inventory_slots(inv);
|
||||
s.run(r#"TT:SetOwner(getglobal("SlotG"), "ANCHOR_RIGHT"); TT:SetInventoryItem("player", 16)"#)
|
||||
.unwrap();
|
||||
let texts: Vec<String> = lines_of(&mut s).into_iter().map(|(t, _)| t).collect();
|
||||
assert_eq!(texts, vec!["Gift", "[WRAPPED_BY Santa]"]);
|
||||
assert!(s.take_errors().is_empty());
|
||||
}
|
||||
|
||||
/// A wrapped gift in a trade names its giver on both sides. The player's slot is a live item
|
||||
/// object: its WRAPPED flag takes the gift arm (`0x52b789`) and the object's `ITEM_FIELD_GIFTCREATOR`
|
||||
/// names the giver, whether the slot came from a click or from the app. The partner's is a block:
|
||||
/// its wrapped word (`0x534559`) takes the same arm and its gift creator (`0x534574`) names the
|
||||
/// giver. A block whose wrapped word is clear ignores the giver, and an unresolved one adds nothing.
|
||||
#[test]
|
||||
fn a_wrapped_gift_in_trade_names_its_giver_on_both_sides() {
|
||||
let mut s = script();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(r#"ITEM_WRAPPED_BY = "[WRAPPED_BY %s]" ITEM_CREATED_BY = "[CREATED_BY %s]""#)
|
||||
.unwrap();
|
||||
s.set_item_template(
|
||||
5042,
|
||||
ItemTemplateView {
|
||||
name: "Gift".into(),
|
||||
quality: 1,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
local a = CreateFrame("Button", "SlotT"); a:SetPoint("CENTER", 0, 0); a:SetWidth(10); a:SetHeight(10)
|
||||
CreateFrame("GameTooltip", "TT", nil, "TestTooltipTemplate")
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let row = |object_flags: Option<u32>, wrapped: bool, gift: Option<&str>| TradeSlotItem {
|
||||
item_id: 5042,
|
||||
name: Some("Gift".into()),
|
||||
count: 1,
|
||||
quality: Some(1),
|
||||
object_flags,
|
||||
wrapped,
|
||||
gift_creator: gift.map(str::to_string),
|
||||
..Default::default()
|
||||
};
|
||||
let mut st = TradeState::default();
|
||||
st.player.slots[0] = Some(row(Some(0x8), true, Some("Santa")));
|
||||
st.player.slots[1] = Some(row(Some(0), false, Some("Santa")));
|
||||
st.target.slots[0] = Some(row(None, true, Some("Santa")));
|
||||
st.target.slots[1] = Some(row(None, false, Some("Santa")));
|
||||
st.target.slots[2] = Some(row(None, true, None));
|
||||
s.set_trade(Some(st));
|
||||
let texts = |s: &mut UiScript, call: &str| -> Vec<String> {
|
||||
s.run(&format!("TT:SetOwner(SlotT, 'ANCHOR_RIGHT') {call}"))
|
||||
.unwrap();
|
||||
lines_of(s).into_iter().map(|(t, _)| t).collect()
|
||||
};
|
||||
let gift = vec!["Gift".to_string(), "[WRAPPED_BY Santa]".to_string()];
|
||||
assert_eq!(texts(&mut s, "TT:SetTradePlayerItem(1)"), gift, "player");
|
||||
assert_eq!(texts(&mut s, "TT:SetTradeTargetItem(1)"), gift, "target");
|
||||
assert_eq!(
|
||||
texts(&mut s, "TT:SetTradePlayerItem(2)"),
|
||||
vec!["Gift"],
|
||||
"unwrapped"
|
||||
);
|
||||
assert_eq!(
|
||||
texts(&mut s, "TT:SetTradeTargetItem(2)"),
|
||||
vec!["Gift"],
|
||||
"unwrapped block"
|
||||
);
|
||||
assert_eq!(
|
||||
texts(&mut s, "TT:SetTradeTargetItem(3)"),
|
||||
vec!["Gift"],
|
||||
"giver unresolved"
|
||||
);
|
||||
|
||||
// The player's click takes the giver from the bag record it places.
|
||||
s.open_trade_window();
|
||||
s.set_trade(Some(TradeState::default()));
|
||||
s.set_container(
|
||||
0,
|
||||
Some(ContainerState {
|
||||
name: Some("Backpack".into()),
|
||||
num_slots: 1,
|
||||
slots: HashMap::from([(
|
||||
1,
|
||||
ContainerSlot {
|
||||
item_id: 5042,
|
||||
guid: 0x5042,
|
||||
count: 1,
|
||||
quality: Some(1),
|
||||
link: Some("|cffffffff|Hitem:5042:0:0:0|h[Gift]|h|r".into()),
|
||||
creator: Some("Geoffrey".into()),
|
||||
gift_creator: Some("Santa".into()),
|
||||
flags: 0x8,
|
||||
..Default::default()
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
);
|
||||
s.run("PickupContainerItem(0, 1) ClickTradeButton(1)")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
texts(&mut s, "TT:SetTradePlayerItem(1)"),
|
||||
gift,
|
||||
"placed by a click"
|
||||
);
|
||||
assert!(s.take_errors().is_empty());
|
||||
}
|
||||
|
||||
/// Bonding `[record+0x194]` in 1..5 decides whether a bind line prints; a runtime-bound instance
|
||||
/// (`0x5da2c0`: the soulbound flag, or a live enchant slot that binds) prints `ITEM_SOULBOUND`,
|
||||
/// or `ITEM_BIND_QUEST` for the quest kinds; else the jump table `0x52e4fc` picks 1 picked up,
|
||||
|
||||
@@ -37,7 +37,21 @@ fn key_slot(entry: u32) -> LockSlotView {
|
||||
/// A locked box (item level 20) in the backpack, whose lock is `slots`, with the player's means
|
||||
/// of opening it `openers`.
|
||||
fn fixture(slots: Vec<LockSlotView>, openers: Option<LockOpeners>) -> UiScript {
|
||||
fixture_rows(LOCK_ID, vec![(LOCK_ID, slots)], openers)
|
||||
}
|
||||
|
||||
/// [`fixture`] with the box's template naming lock `template_lock` and the store holding `rows`.
|
||||
fn fixture_rows(
|
||||
template_lock: u32,
|
||||
rows: Vec<(u32, Vec<LockSlotView>)>,
|
||||
openers: Option<LockOpeners>,
|
||||
) -> UiScript {
|
||||
let mut s = item_script();
|
||||
s.run(
|
||||
r#"LOCKED_WITH_ITEM = "[LOCKED_WITH_ITEM %s]"
|
||||
LOCKED_WITH_SPELL_KNOWN = "[LOCKED_WITH_SPELL_KNOWN %s]""#,
|
||||
)
|
||||
.unwrap();
|
||||
s.set_screen_size(800.0, 600.0);
|
||||
s.run(
|
||||
r#"
|
||||
@@ -53,7 +67,7 @@ fn fixture(slots: Vec<LockSlotView>, openers: Option<LockOpeners>) -> UiScript {
|
||||
name: "Lockbox".into(),
|
||||
quality: 1,
|
||||
item_level: 20,
|
||||
lock_id: LOCK_ID,
|
||||
lock_id: template_lock,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
@@ -74,7 +88,8 @@ fn fixture(slots: Vec<LockSlotView>, openers: Option<LockOpeners>) -> UiScript {
|
||||
)]),
|
||||
}),
|
||||
);
|
||||
s.set_lock_rows(HashMap::from([(LOCK_ID, slots)]));
|
||||
s.set_lock_rows(rows.into_iter().collect());
|
||||
s.set_lock_type_names(HashMap::from([(1, "Pick Lock".to_string())]));
|
||||
if let Some(o) = openers {
|
||||
s.set_lock_openers(o);
|
||||
}
|
||||
@@ -175,3 +190,262 @@ fn the_first_slot_to_open_the_lock_decides_the_colour() {
|
||||
let mut s = fixture(vec![skill_slot(1, 25), key_slot(KEY)], Some(strong));
|
||||
assert_eq!(locked_color(&mut s), ORANGE, "the skill is first");
|
||||
}
|
||||
|
||||
/// The tooltip's lines under a bag hover.
|
||||
fn bag_lines(s: &mut UiScript) -> Vec<(String, [f32; 4])> {
|
||||
s.run("TT:SetOwner(Slot, 'ANCHOR_RIGHT') TT:SetBagItem(0, 1)")
|
||||
.unwrap();
|
||||
lines_of(s)
|
||||
}
|
||||
|
||||
fn line<'a>(lines: &'a [(String, [f32; 4])], text: &str) -> Option<&'a (String, [f32; 4])> {
|
||||
lines.iter().find(|(t, _)| t == text)
|
||||
}
|
||||
|
||||
fn key_template(s: &mut UiScript) {
|
||||
s.set_item_template(
|
||||
KEY,
|
||||
ItemTemplateView {
|
||||
name: "Skeleton Key".into(),
|
||||
quality: 1,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// A key slot (`0x52bf30`) names the key from the item cache in white (`0x52bf93`), under LOCKED,
|
||||
/// whether or not the player holds it.
|
||||
#[test]
|
||||
fn a_key_lock_names_its_key_in_white_under_locked() {
|
||||
for held in [false, true] {
|
||||
let mut openers = LockOpeners::default();
|
||||
if held {
|
||||
openers.held.insert(KEY);
|
||||
}
|
||||
let mut s = fixture(vec![key_slot(KEY)], Some(openers));
|
||||
key_template(&mut s);
|
||||
let lines = bag_lines(&mut s);
|
||||
let at = |t: &str| lines.iter().position(|(l, _)| l == t);
|
||||
assert_eq!(
|
||||
at("[LOCKED_WITH_ITEM Skeleton Key]"),
|
||||
at("[LOCKED]").map(|i| i + 1),
|
||||
"{lines:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
line(&lines, "[LOCKED_WITH_ITEM Skeleton Key]").unwrap().1,
|
||||
[1.0; 4]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A key the item cache lacks adds nothing (`0x52bf67`) and is asked for, so the hover's repaint
|
||||
/// draws it on arrival.
|
||||
#[test]
|
||||
fn a_key_the_cache_lacks_adds_no_line_and_is_asked_for() {
|
||||
let mut s = fixture(vec![key_slot(KEY)], Some(LockOpeners::default()));
|
||||
let lines = bag_lines(&mut s);
|
||||
assert!(line(&lines, "[LOCKED]").is_some());
|
||||
assert!(
|
||||
lines.iter().all(|(t, _)| !t.starts_with("[LOCKED_WITH")),
|
||||
"{lines:?}"
|
||||
);
|
||||
assert!(s.take_item_stat_asks().contains(&KEY));
|
||||
}
|
||||
|
||||
/// A skill slot names the lock type, in the ramp colour of the spell the walk tried last
|
||||
/// (`0x52beae`), the same colour `LOCKED` takes when the spell reaches the requirement.
|
||||
#[test]
|
||||
fn a_skill_lock_names_its_lock_type_in_the_ramp_colour() {
|
||||
for (points, want) in [(25, ORANGE), (50, YELLOW), (75, LOCK_GREEN), (125, GRAY)] {
|
||||
let mut s = fixture(vec![skill_slot(1, 25)], Some(picker(points)));
|
||||
let lines = bag_lines(&mut s);
|
||||
let sub = line(&lines, "[LOCKED_WITH_SPELL_KNOWN Pick Lock]")
|
||||
.unwrap_or_else(|| panic!("no sub-line at {points}: {lines:?}"));
|
||||
assert_eq!(sub.1, want, "{points} points");
|
||||
assert_eq!(line(&lines, "[LOCKED]").unwrap().1, want);
|
||||
}
|
||||
}
|
||||
|
||||
/// The line shows for any known opener of the lock type, even one whose skill falls short
|
||||
/// (`0x5da6cc` records it before the compare): red, under a red `LOCKED`.
|
||||
#[test]
|
||||
fn a_known_opener_below_the_skill_still_names_the_lock_type_in_red() {
|
||||
let mut s = fixture(vec![skill_slot(1, 25)], Some(picker(24)));
|
||||
let lines = bag_lines(&mut s);
|
||||
assert_eq!(line(&lines, "[LOCKED]").unwrap().1, RED);
|
||||
assert_eq!(
|
||||
line(&lines, "[LOCKED_WITH_SPELL_KNOWN Pick Lock]")
|
||||
.unwrap()
|
||||
.1,
|
||||
RED
|
||||
);
|
||||
}
|
||||
|
||||
/// No known spell of the lock type, or no active player (the walk returns before it reads a
|
||||
/// slot, `0x5da60b`), adds nothing (`0x52bea2`).
|
||||
#[test]
|
||||
fn a_skill_lock_with_no_known_opener_adds_no_line() {
|
||||
for openers in [
|
||||
Some(LockOpeners::default()),
|
||||
// A herbalism spell against a lockpicking lock.
|
||||
Some(LockOpeners {
|
||||
spells: vec![LockOpener {
|
||||
lock_type: 2,
|
||||
points: 300,
|
||||
}],
|
||||
held: HashSet::new(),
|
||||
}),
|
||||
None,
|
||||
] {
|
||||
let mut s = fixture(vec![skill_slot(1, 25)], openers.clone());
|
||||
let lines = bag_lines(&mut s);
|
||||
assert!(line(&lines, "[LOCKED]").is_some());
|
||||
assert!(
|
||||
lines.iter().all(|(t, _)| !t.starts_with("[LOCKED_WITH")),
|
||||
"{openers:?}: {lines:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A lock type the store lacks reads `UNKNOWN` (`0x52bf00`); the line is the first slot's alone,
|
||||
/// so a lock whose first slot is empty adds none (`0x52be5d`).
|
||||
#[test]
|
||||
fn the_sub_line_reads_the_first_slot_and_names_a_missing_type_unknown() {
|
||||
let mut s = fixture(
|
||||
vec![skill_slot(9, 25)],
|
||||
Some(LockOpeners {
|
||||
spells: vec![LockOpener {
|
||||
lock_type: 9,
|
||||
points: 25,
|
||||
}],
|
||||
held: HashSet::new(),
|
||||
}),
|
||||
);
|
||||
let lines = bag_lines(&mut s);
|
||||
assert!(
|
||||
line(&lines, "[LOCKED_WITH_SPELL_KNOWN UNKNOWN]").is_some(),
|
||||
"{lines:?}"
|
||||
);
|
||||
let mut s = fixture(
|
||||
vec![LockSlotView::default(), skill_slot(1, 25)],
|
||||
Some(picker(300)),
|
||||
);
|
||||
let lines = bag_lines(&mut s);
|
||||
assert!(line(&lines, "[LOCKED]").is_some());
|
||||
assert!(
|
||||
lines.iter().all(|(t, _)| !t.starts_with("[LOCKED_WITH")),
|
||||
"{lines:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A record whose lock id has no row falls back to the block's row (`0x52bd76`): the trade
|
||||
/// target's lock names the row, and `LOCKED` and its sub-line follow that row. With no row in
|
||||
/// either, there is no lock to draw (`0x52bda9`).
|
||||
#[test]
|
||||
fn a_missing_record_row_falls_back_to_the_blocks_row() {
|
||||
const MISSING: u32 = 99;
|
||||
let mut s = fixture_rows(
|
||||
MISSING,
|
||||
vec![(LOCK_ID, vec![skill_slot(1, 25)])],
|
||||
Some(picker(25)),
|
||||
);
|
||||
let trade = |lock_id: u32| {
|
||||
let mut st = TradeState::default();
|
||||
st.target.slots[0] = Some(TradeSlotItem {
|
||||
item_id: LOCKBOX,
|
||||
name: Some("Lockbox".into()),
|
||||
count: 1,
|
||||
lock_id,
|
||||
..Default::default()
|
||||
});
|
||||
st
|
||||
};
|
||||
s.set_trade(Some(trade(LOCK_ID)));
|
||||
s.run("TT:SetOwner(Slot, 'ANCHOR_RIGHT') TT:SetTradeTargetItem(1)")
|
||||
.unwrap();
|
||||
let lines = lines_of(&mut s);
|
||||
assert_eq!(line(&lines, "[LOCKED]").unwrap().1, ORANGE, "{lines:?}");
|
||||
assert!(line(&lines, "[LOCKED_WITH_SPELL_KNOWN Pick Lock]").is_some());
|
||||
|
||||
// Neither row exists: no LOCKED, no sub-line.
|
||||
s.set_trade(Some(trade(MISSING)));
|
||||
s.run("TT:SetOwner(Slot, 'ANCHOR_RIGHT') TT:SetTradeTargetItem(1)")
|
||||
.unwrap();
|
||||
let lines = lines_of(&mut s);
|
||||
assert!(line(&lines, "[LOCKED]").is_none(), "{lines:?}");
|
||||
|
||||
// A bag item (no block) whose record names a missing row is not locked either.
|
||||
let lines = bag_lines(&mut s);
|
||||
assert!(line(&lines, "[LOCKED]").is_none(), "{lines:?}");
|
||||
}
|
||||
|
||||
/// A lock type whose name is blank is a row, not a missing one: only a missing row reads `UNKNOWN`
|
||||
/// (`0x52bede`..`0x52bef2`), a blank name fills the line as it is (`0x52bef4`).
|
||||
#[test]
|
||||
fn a_blank_lock_type_name_fills_the_line_and_is_not_unknown() {
|
||||
let mut s = fixture(vec![skill_slot(5, 25)], Some(picker_for(5, 25)));
|
||||
s.set_lock_type_names(HashMap::from([(5, String::new())]));
|
||||
let lines = bag_lines(&mut s);
|
||||
assert!(
|
||||
line(&lines, "[LOCKED_WITH_SPELL_KNOWN ]").is_some(),
|
||||
"{lines:?}"
|
||||
);
|
||||
assert!(line(&lines, "[LOCKED_WITH_SPELL_KNOWN UNKNOWN]").is_none());
|
||||
}
|
||||
|
||||
fn picker_for(lock_type: u32, points: i32) -> LockOpeners {
|
||||
LockOpeners {
|
||||
spells: vec![LockOpener { lock_type, points }],
|
||||
held: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// OPENABLE reads the record's raw lock id: a nonzero one needs the object UNLOCKED (`0x52e2fc`),
|
||||
/// with no row check, so a lock id without a Lock row (no LOCKED line to draw) still withholds it.
|
||||
#[test]
|
||||
fn openable_waits_for_unlocked_on_a_lock_id_with_no_row() {
|
||||
const MISSING: u32 = 99;
|
||||
let mut s = fixture_rows(MISSING, Vec::new(), None);
|
||||
s.set_item_template(
|
||||
LOCKBOX,
|
||||
ItemTemplateView {
|
||||
name: "Lockbox".into(),
|
||||
quality: 1,
|
||||
flags: 0x4,
|
||||
lock_id: MISSING,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let slot = |flags| ContainerSlot {
|
||||
item_id: LOCKBOX,
|
||||
count: 1,
|
||||
quality: Some(1),
|
||||
flags,
|
||||
..Default::default()
|
||||
};
|
||||
let hover = |s: &mut UiScript, flags| {
|
||||
s.set_container(
|
||||
0,
|
||||
Some(ContainerState {
|
||||
name: Some("Backpack".into()),
|
||||
num_slots: 16,
|
||||
slots: HashMap::from([(1, slot(flags))]),
|
||||
}),
|
||||
);
|
||||
bag_lines(s)
|
||||
};
|
||||
let lines = hover(&mut s, 0);
|
||||
assert!(
|
||||
line(&lines, "[LOCKED]").is_none(),
|
||||
"no row, no lock: {lines:?}"
|
||||
);
|
||||
assert!(
|
||||
line(&lines, "[ITEM_OPENABLE]").is_none(),
|
||||
"locked by id, not unlocked: {lines:?}"
|
||||
);
|
||||
let lines = hover(&mut s, 0x4);
|
||||
assert!(
|
||||
line(&lines, "[ITEM_OPENABLE]").is_some(),
|
||||
"UNLOCKED: {lines:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -339,6 +339,10 @@ fn live_objects_and_blocks_reach_the_flag() {
|
||||
let mut s = fixture();
|
||||
s.run(r#"ITEM_DURATION_HOURS = "[DURATION %d h]" LOCKED = "[LOCKED]""#)
|
||||
.unwrap();
|
||||
s.set_lock_rows(std::collections::HashMap::from([
|
||||
(7, Vec::new()),
|
||||
(9, Vec::new()),
|
||||
]));
|
||||
s.set_item_template(
|
||||
8300,
|
||||
ItemTemplateView {
|
||||
|
||||
@@ -148,6 +148,7 @@ fn slot_instance(s: &crate::script::ContainerSlot, openable_source: bool) -> ren
|
||||
name: s.link.as_deref().and_then(link_name).map(str::to_string),
|
||||
durability: s.durability,
|
||||
creator: s.creator.clone(),
|
||||
gift_creator: s.gift_creator.clone(),
|
||||
has_text: s.readable,
|
||||
flags: s.flags,
|
||||
already_bound: s.already_bound,
|
||||
@@ -157,6 +158,7 @@ fn slot_instance(s: &crate::script::ContainerSlot, openable_source: bool) -> ren
|
||||
duration_ms: s.duration_ms,
|
||||
object: true,
|
||||
block_lock_id: 0,
|
||||
block_wrapped: false,
|
||||
cooldown_ms: 0,
|
||||
}
|
||||
}
|
||||
@@ -182,6 +184,7 @@ pub(super) fn owned_instance(model: &Model, item_id: u32) -> Option<render::Item
|
||||
name: s.name.clone(),
|
||||
durability: s.durability,
|
||||
creator: s.creator.clone(),
|
||||
gift_creator: s.gift_creator.clone(),
|
||||
flags: s.flags,
|
||||
already_bound: s.already_bound,
|
||||
enchants: s.enchants.clone(),
|
||||
@@ -451,6 +454,7 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
name: s.name.clone(),
|
||||
durability: s.durability,
|
||||
creator: s.creator.clone(),
|
||||
gift_creator: s.gift_creator.clone(),
|
||||
has_text: false,
|
||||
flags: s.flags,
|
||||
already_bound: s.already_bound,
|
||||
@@ -465,6 +469,7 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
// inspected unit's item is a block (`0x532ee0`).
|
||||
object: unit.eq_ignore_ascii_case("player"),
|
||||
block_lock_id: 0,
|
||||
block_wrapped: false,
|
||||
// The player's own item answers the cooldown query
|
||||
// (`0x5331ec`), unless it is charged and spent
|
||||
// (`0x53318a`-`0x53319b`, the feed's `None`); an inspected
|
||||
@@ -657,10 +662,13 @@ pub(super) fn install_methods(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
clear(lua, h);
|
||||
let inst = render::ItemInstance {
|
||||
name: item.name.clone(),
|
||||
// The target side's block carries the partner's lock id (`0x534563`); the
|
||||
// player's is zero (`0x534341`), but its slot is a live item object, whose
|
||||
// flags the LOCKED gate reads (`0x5342ed`, `0x5343da`).
|
||||
// The target side's block carries the partner's lock id (`0x534563`), wrapped
|
||||
// word (`0x534559`) and gift creator (`0x534574`); the player's lock id is
|
||||
// zero (`0x534341`), but its slot is a live item object, whose flags the
|
||||
// LOCKED and wrapped gates read (`0x5342ed`, `0x5343da`, `0x52b789`).
|
||||
block_lock_id: if player_side { 0 } else { item.lock_id },
|
||||
block_wrapped: !player_side && item.wrapped,
|
||||
gift_creator: item.gift_creator.clone(),
|
||||
object: item.object_flags.is_some(),
|
||||
flags: item.object_flags.unwrap_or(0),
|
||||
..Default::default()
|
||||
|
||||
@@ -25,6 +25,9 @@ pub(in crate::script) struct ItemInstance {
|
||||
/// The resolved `ITEM_FIELD_CREATOR` name (the reference's `0x55f080`); `None` while it is
|
||||
/// unresolved, which prints no line either (`0x52e209`).
|
||||
pub creator: Option<String>,
|
||||
/// The resolved `ITEM_FIELD_GIFTCREATOR` name (`0x55f080` at `0x52e29d`) of a wrapped item;
|
||||
/// `None` while it is unresolved (`0x52e2a4`) or the field is 0 (`0x52e288`): no line either way.
|
||||
pub gift_creator: Option<String>,
|
||||
/// `ITEM_FIELD_ITEM_TEXT_ID` ≠ 0, a letter's copy: the creator line reads WRITTEN_BY
|
||||
/// (`0x52e223`) and READABLE shows (`0x52e348`).
|
||||
pub has_text: bool,
|
||||
@@ -54,6 +57,10 @@ pub(in crate::script) struct ItemInstance {
|
||||
/// none or no block. With no lock row from the template it names one (`0x52bd78`..`0x52bda9`),
|
||||
/// and a nonzero one draws LOCKED even without an object (`0x52bdd2`).
|
||||
pub block_lock_id: u32,
|
||||
/// The block's wrapped word (`[this+0x42c]`, set by the trade target's setter, `0x534559`): it
|
||||
/// takes the gift arm and the no-id-source fork like an object's WRAPPED flag (`0x52b789`,
|
||||
/// `0x52c991`).
|
||||
pub block_wrapped: bool,
|
||||
/// The remaining use cooldown in ms (`[this+0x408]`, `start + duration - now`), 0 for none. Only
|
||||
/// the three setters that query the item's cooldown fill it, `SetBagItem` (`0x53488c`),
|
||||
/// `SetInventoryItem` (`0x533226`) and `SetAction`'s item leg (`0x5324d5`), and each does so
|
||||
@@ -61,6 +68,13 @@ pub(in crate::script) struct ItemInstance {
|
||||
pub cooldown_ms: u32,
|
||||
}
|
||||
|
||||
impl ItemInstance {
|
||||
/// The wrapped test the builder runs once: the object's WRAPPED flag, or the block's word.
|
||||
fn wrapped(&self) -> bool {
|
||||
self.flags & 0x8 != 0 || self.block_wrapped
|
||||
}
|
||||
}
|
||||
|
||||
/// The set block's blank gold line, the reference's literal `0x854b2c`: a space and a newline. An
|
||||
/// empty string would be no row, as `AddLine` (`0x530270`) drops an empty left text uncounted
|
||||
/// (`0x5302a9`). `" \n"` is one row: the stepper `0x5c7470` consumes the break with the space
|
||||
@@ -86,8 +100,7 @@ pub(super) struct BuilderFlags {
|
||||
/// (`0x52ce0d`). The
|
||||
/// setters push it as 1 or nil; the stock buttons arm `updateTooltip` from it. The LOCKED line
|
||||
/// sets it on every lock path (`0x52bfa6`, after `0x52be58`, whether or not a `LOCKED_WITH_*`
|
||||
/// sub-line follows), as ITEM_COOLDOWN_TIME does (`0x52e140`). Not built: ITEM_WRAPPED_BY and the
|
||||
/// `LOCKED_WITH_*` sub-line.
|
||||
/// sub-line follows), as ITEM_COOLDOWN_TIME does (`0x52e140`).
|
||||
pub(super) fn render_view_flagged(
|
||||
lua: &Lua,
|
||||
this: &Table,
|
||||
@@ -199,22 +212,17 @@ pub(super) fn render_view_flagged(
|
||||
if v.start_quest != 0 {
|
||||
keyed("ITEM_STARTS_QUEST", &[], WHITE, false)?;
|
||||
}
|
||||
// LOCKED (`0x52bdde`..`0x52be58`), for a lock row the template or the block names
|
||||
// (`0x52bd57`..`0x52bda9`) and a source that passes the gate (`0x52bdb3`..`0x52bdd8`): an
|
||||
// item object lacking UNLOCKED `0x4`, or a block with a lock id of its own. A template
|
||||
// source has neither, so it draws no LOCKED. Every lock path then sets the builder's
|
||||
// return value (`0x52bfa6`).
|
||||
let block_lock = inst.map_or(0, |i| i.block_lock_id);
|
||||
let has_row = v.lock_id != 0 || block_lock != 0;
|
||||
let passes = block_lock != 0 || inst.is_some_and(|i| i.object && i.flags & 0x4 == 0);
|
||||
if has_row && passes {
|
||||
// The record's row first, else the block's (`0x52bd57`, `0x52bd78`).
|
||||
let row = if v.lock_id != 0 {
|
||||
v.lock_id
|
||||
} else {
|
||||
block_lock
|
||||
};
|
||||
keyed("LOCKED", &[], lock_color(lua, row, v.item_level), false)?;
|
||||
// LOCKED (`0x52bdde`..`0x52be58`) and the line under it. The lock row is the record's own,
|
||||
// else the block's (`0x52bd57`..`0x52bda9`): a row the store lacks falls through to the
|
||||
// block, and with neither there is no lock. The source must pass the gate (`0x52bdb3`..
|
||||
// `0x52bdd8`): an item object lacking UNLOCKED `0x4`, or a block with a lock id of its
|
||||
// own. A template source has neither, so it draws no LOCKED. Every lock path then sets
|
||||
// the builder's return value (`0x52bfa6`).
|
||||
if let Some(lock) = lock_lines(lua, v, inst) {
|
||||
keyed("LOCKED", &[], lock.color, false)?;
|
||||
if let Some(sub) = &lock.sub {
|
||||
keyed(sub.key, &[Arg::S(&sub.arg)], sub.color, false)?;
|
||||
}
|
||||
live = true;
|
||||
}
|
||||
}
|
||||
@@ -428,7 +436,7 @@ pub(super) fn render_view_flagged(
|
||||
// No id source (`0x52c991`): a wrapped gift, or no item object and no instance block
|
||||
// (`+0x440 == 0`), here a hover passing no [`ItemInstance`]. The fork tests the block's
|
||||
// presence, not its contents (`0x52c9a3`), so a block source never shows the placeholder.
|
||||
let no_id_source = inst.is_none_or(|i| i.flags & 0x8 != 0);
|
||||
let no_id_source = inst.is_none_or(ItemInstance::wrapped);
|
||||
if no_id_source && !signable && v.random_property != 0 {
|
||||
keyed("ITEM_RANDOM_ENCHANT", &[], GREEN, false)?;
|
||||
}
|
||||
@@ -662,19 +670,23 @@ pub(super) fn render_view_flagged(
|
||||
// source (`None`) stops, so a link, loot, mail or auction hover, which has an instance but no
|
||||
// object, still reaches READABLE.
|
||||
let Some(inst) = inst else { return Ok(live) };
|
||||
// The creator line (`0x52e1b1..0x52e2db`), white `0xc0cf60`: ITEM_WRITTEN_BY for a letter, else
|
||||
// ITEM_CREATED_BY, green by its own `|cff00ff00`. A wrapped instance (`0x8`, gate `0x52b7b0`)
|
||||
// takes ITEM_WRAPPED_BY off `ITEM_FIELD_GIFTCREATOR` in the reference; that is not built, so a
|
||||
// wrapped instance prints no creator line.
|
||||
if inst.flags & 0x8 == 0 {
|
||||
if let Some(name) = &inst.creator {
|
||||
let key = if inst.has_text {
|
||||
"ITEM_WRITTEN_BY"
|
||||
} else {
|
||||
"ITEM_CREATED_BY"
|
||||
};
|
||||
keyed(key, &[Arg::S(name)], WHITE, false)?;
|
||||
// The creator line (`0x52e1b1..0x52e2db`), white `0xc0cf60`. The arm is picked once, at
|
||||
// `0x52b78f`..`0x52b7b0`: an item object with WRAPPED `0x8` (`0x52b789`) takes the gift arm,
|
||||
// and so does a block whose `+0x42c` is set (the trade target's). The gift arm adds
|
||||
// ITEM_WRAPPED_BY off `ITEM_FIELD_GIFTCREATOR` (`0x52e32a`) or the block's `+0x418`, and never
|
||||
// the creator's line. Any other source adds
|
||||
// ITEM_WRITTEN_BY for a letter, else ITEM_CREATED_BY, green by its own `|cff00ff00`.
|
||||
if inst.wrapped() {
|
||||
if let Some(name) = &inst.gift_creator {
|
||||
keyed("ITEM_WRAPPED_BY", &[Arg::S(name)], WHITE, false)?;
|
||||
}
|
||||
} else if let Some(name) = &inst.creator {
|
||||
let key = if inst.has_text {
|
||||
"ITEM_WRITTEN_BY"
|
||||
} else {
|
||||
"ITEM_CREATED_BY"
|
||||
};
|
||||
keyed(key, &[Arg::S(name)], WHITE, false)?;
|
||||
}
|
||||
// One green line, OPENABLE winning (`0x52e2f2..0x52e35d`). Openable: a p6=0 source and either
|
||||
// the loot flag `0x4` behind its lock gate (a lock needs UNLOCKED) or a wrapped gift (`0x200`
|
||||
@@ -691,21 +703,87 @@ pub(super) fn render_view_flagged(
|
||||
Ok(live)
|
||||
}
|
||||
|
||||
/// The colour of `LOCKED` for lock row `row` of an item of `item_level` (`0x52bdde`..`0x52be3d`):
|
||||
/// red unless the lock check `0x5da5e0` finds an opener, then the margin ramp `0x529fa0` for a
|
||||
/// known spell, else the green `0xc0d420` for a held key or a lock with no key or skill slot. A
|
||||
/// row the app never pushed (no `Lock.dbc`) has nothing to open it, so it stays red.
|
||||
fn lock_color(lua: &Lua, row: u32, item_level: u32) -> [f32; 4] {
|
||||
/// What the lock block draws: `LOCKED`'s colour and the line under it.
|
||||
struct LockLines {
|
||||
color: [f32; 4],
|
||||
sub: Option<LockSubLine>,
|
||||
}
|
||||
|
||||
/// The line under `LOCKED`, `key` filled with `arg`.
|
||||
struct LockSubLine {
|
||||
key: &'static str,
|
||||
arg: String,
|
||||
color: [f32; 4],
|
||||
}
|
||||
|
||||
/// The lock block of the item builder (`0x52bd57`..`0x52bfa6`), `None` when it draws nothing.
|
||||
///
|
||||
/// `LOCKED`'s colour is red unless the lock check `0x5da5e0` finds an opener, then the margin ramp
|
||||
/// `0x529fa0` for a known spell, else the green `0xc0d420` for a held key or a lock with no key or
|
||||
/// skill slot (`0x52bdde`..`0x52be3d`). The first slot's type then picks the line under it
|
||||
/// (`0x52be5d`):
|
||||
/// - a key (1) names the key item from the item cache, in white, and nothing on a miss
|
||||
/// (`0x52bf30`..`0x52bfa1`); a miss asks for the template, and the line appears on a redraw,
|
||||
/// which benilla runs only where stock re-runs `OnEnter` (a bag hover does, the trade
|
||||
/// target's does not);
|
||||
/// - a skill (2) walks the lock a second time (`0x52be98`) and names the lock type in the ramp's
|
||||
/// colour for the last open-lock spell it tried, even one whose skill falls short; no known
|
||||
/// opener of the type adds nothing (`0x52bea2`);
|
||||
/// - anything else adds nothing.
|
||||
fn lock_lines(lua: &Lua, v: &ItemTemplateView, inst: Option<&ItemInstance>) -> Option<LockLines> {
|
||||
use crate::script::lock_check::walk_lock;
|
||||
use crate::script::{check_lock, LockCheck};
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let Some(slots) = model.lock_rows.get(&row) else {
|
||||
return RED;
|
||||
let mut guard = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let model = &mut *guard;
|
||||
let block_lock = inst.map_or(0, |i| i.block_lock_id);
|
||||
let row = if model.lock_rows.contains_key(&v.lock_id) {
|
||||
v.lock_id
|
||||
} else if block_lock != 0 && model.lock_rows.contains_key(&block_lock) {
|
||||
block_lock
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
match check_lock(slots, item_level, model.lock_openers.as_ref()) {
|
||||
let passes = block_lock != 0 || inst.is_some_and(|i| i.object && i.flags & 0x4 == 0);
|
||||
if !passes {
|
||||
return None;
|
||||
}
|
||||
let slots = &model.lock_rows[&row];
|
||||
let openers = model.lock_openers.as_ref();
|
||||
let color = match check_lock(slots, v.item_level, openers) {
|
||||
LockCheck::Closed => RED,
|
||||
LockCheck::Open => LOCK_GREEN,
|
||||
LockCheck::Skill { required, points } => skill_color(required, points),
|
||||
}
|
||||
};
|
||||
let sub =
|
||||
match slots.first() {
|
||||
Some(first) if first.key_type == 1 => {
|
||||
let entry = first.index;
|
||||
let name = model.item_templates.get(&entry).map(|t| t.name.clone());
|
||||
if name.is_none() && entry != 0 {
|
||||
model.item_stat_asks.insert(entry);
|
||||
}
|
||||
name.map(|arg| LockSubLine {
|
||||
key: "LOCKED_WITH_ITEM",
|
||||
arg,
|
||||
color: WHITE,
|
||||
})
|
||||
}
|
||||
Some(first) if first.key_type == 2 => walk_lock(slots, v.item_level, openers)
|
||||
.spell
|
||||
.map(|t| LockSubLine {
|
||||
key: "LOCKED_WITH_SPELL_KNOWN",
|
||||
// `UNKNOWN` (`0x838044`) only for a missing row (`0x52bede`..`0x52bef2`); a
|
||||
// blank name stays blank (`0x52bef4`).
|
||||
arg: model
|
||||
.lock_type_names
|
||||
.get(&first.index)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "UNKNOWN".to_string()),
|
||||
color: skill_color(t.required, t.points),
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
Some(LockLines { color, sub })
|
||||
}
|
||||
|
||||
/// [`render_view_flagged`] for a caller that does not answer the flag.
|
||||
|
||||
@@ -38,6 +38,14 @@ pub struct TradeSlotItem {
|
||||
/// slot holds a live item object, which the tooltip builder's LOCKED gate reads (`0x5342ed`);
|
||||
/// the partner's side has none.
|
||||
pub object_flags: Option<u32>,
|
||||
/// The wire's wrapped word: the partner's block `+0x42c` (`0x534559`), which the tooltip's
|
||||
/// wrapped gates read (`0x52c991`, `0x52cc5b`). The player's side reads its object's
|
||||
/// `ITEM_FIELD_FLAGS` instead, so this only mirrors them there.
|
||||
pub wrapped: bool,
|
||||
/// The resolved name of the giver of a wrapped gift (`ITEM_FIELD_GIFTCREATOR` on the player's
|
||||
/// side, the block's `+0x418` on the partner's, `0x534574`); `None` while unresolved or when
|
||||
/// the guid is 0, which prints no `ITEM_WRAPPED_BY` line.
|
||||
pub gift_creator: Option<String>,
|
||||
/// Slot 7 only: the name of the spell being cast on the item, which stock shows in green
|
||||
/// (`TradeFrame.lua:62`); `None` on every other slot and while nothing is cast.
|
||||
pub enchantment: Option<String>,
|
||||
@@ -477,6 +485,8 @@ fn trade_slot_item(model: &Model, item: &cursor::CursorItem) -> TradeSlotItem {
|
||||
inventory_type: s.inventory_type,
|
||||
lock_id: 0,
|
||||
object_flags: Some(s.flags),
|
||||
wrapped: s.flags & 0x8 != 0,
|
||||
gift_creator: s.gift_creator.clone(),
|
||||
enchantment: None,
|
||||
link: s.link.clone(),
|
||||
})
|
||||
@@ -760,6 +770,8 @@ mod tests {
|
||||
inventory_type: 0,
|
||||
lock_id: 0,
|
||||
object_flags: None,
|
||||
wrapped: false,
|
||||
gift_creator: None,
|
||||
enchantment: None,
|
||||
link: Some(format!(
|
||||
"|cffffffff|Hitem:{item_id}:0:0:0|h[Linen Cloth]|h|r"
|
||||
@@ -804,6 +816,8 @@ mod tests {
|
||||
inventory_type: 0,
|
||||
lock_id: 0,
|
||||
object_flags: None,
|
||||
wrapped: false,
|
||||
gift_creator: None,
|
||||
enchantment: None,
|
||||
link: None,
|
||||
});
|
||||
|
||||
+3
-3
@@ -636,7 +636,7 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it
|
||||
- An id outside the seven built windows raises, where the reference serves ten records and silently ignores any other id (`0x4a0be1`), because benilla keeps only the seven and every caller loops `1, NUM_CHAT_WINDOWS`. ([`src/script/chat_window.rs:220`](../crates/benilla-ui/src/script/chat_window.rs#L220))
|
||||
- One past `i32::MAX` clamps where the reference keeps the low 32 bits, for the colours' reason. ([`src/script/chat_window.rs:430`](../crates/benilla-ui/src/script/chat_window.rs#L430))
|
||||
- With no wheel it scales by the strip's, because the reference reads the missing wheel unguarded. ([`src/script/colorselect.rs:364`](../crates/benilla-ui/src/script/colorselect.rs#L364))
|
||||
- A row that no longer resolves is refused, as `0x4c7300` does, because this path dereferences it unchecked (`0x4f9f35`), a latent null deref. ([`src/script/container.rs:658`](../crates/benilla-ui/src/script/container.rs#L658))
|
||||
- A row that no longer resolves is refused, as `0x4c7300` does, because this path dereferences it unchecked (`0x4f9f35`), a latent null deref. ([`src/script/container.rs:661`](../crates/benilla-ui/src/script/container.rs#L661))
|
||||
- A last `spell_id` key breaks the remaining ties (Charge's six ranks all have `spellLevel` 0), because the reference leaves them to its qsort over an array benilla does not reproduce; ascending id is ascending rank in every 1.12 pet-ability family. ([`src/script/craft.rs:104`](../crates/benilla-ui/src/script/craft.rs#L104))
|
||||
- On `Terrain` the reference keeps a spell, action, macro or pet action on the left button; we clear them, because otherwise a spell has no left-click way off the cursor. ([`src/script/cursor.rs:523`](../crates/benilla-ui/src/script/cursor.rs#L523))
|
||||
- Every lock lifts here, bag contents included. The reference's walk of a bag's contents indexes the bag by its own player-array index (`0x495528`, `0x495535`), at least 19, past a bag's slots in every case but the biggest bags, so it normally unlocks none of them; a lock it strands lasts until the item's seat or stack moves, which this does not copy. ([`src/script/cursor.rs:694`](../crates/benilla-ui/src/script/cursor.rs#L694))
|
||||
@@ -694,8 +694,8 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it
|
||||
- The reference reads uninitialised fields before the seed; we answer the first legal design because that value is undefined. ([`src/script/tabard.rs:103`](../crates/benilla-ui/src/script/tabard.rs#L103))
|
||||
- Returns nothing. The handler ends `mov eax,1` with nothing pushed (`0x4dcd7c`), so Lua 5.0 hands back the stack top, the node argument: an accident of the handler, not an API, and copying a stack-top leak would copy a bug. ([`src/script/taxi.rs:138`](../crates/benilla-ui/src/script/taxi.rs#L138))
|
||||
- A handler that unregisters the saved next ends the dispatch, because the reference then reads a freed node. ([`src/script/tick.rs:30`](../crates/benilla-ui/src/script/tick.rs#L30))
|
||||
- While the template is in flight the reference leaves the tooltip empty (`0x52b6a3`); this shows the slot's name, because a blank plate under an on-screen name reads as broken. ([`src/script/tooltip_item/mod.rs:611`](../crates/benilla-ui/src/script/tooltip_item/mod.rs#L611))
|
||||
- The reference's padding repeats a mana stat once per zero entry; not emulated, since only the internal test ring 6674 has one. ([`src/script/tooltip_item/render.rs:375`](../crates/benilla-ui/src/script/tooltip_item/render.rs#L375))
|
||||
- While the template is in flight the reference leaves the tooltip empty (`0x52b6a3`); this shows the slot's name, because a blank plate under an on-screen name reads as broken. ([`src/script/tooltip_item/mod.rs:616`](../crates/benilla-ui/src/script/tooltip_item/mod.rs#L616))
|
||||
- The reference's padding repeats a mana stat once per zero entry; not emulated, since only the internal test ring 6674 has one. ([`src/script/tooltip_item/render.rs:383`](../crates/benilla-ui/src/script/tooltip_item/render.rs#L383))
|
||||
- The unknown-skill "Requires %s" shares it, though its reference red is `0xffff0000`, because a 32/255 difference in G and B does not show. ([`src/script/tooltip_unit.rs:30`](../crates/benilla-ui/src/script/tooltip_unit.rs#L30))
|
||||
- The selection clears to 0, because the header the reference re-selects (`0x4d7b42`, reading 1) has no spell id; the stock window's `> 1` test treats 0 and 1 alike. ([`src/script/trainer/mod.rs:197`](../crates/benilla-ui/src/script/trainer/mod.rs#L197))
|
||||
- A header clears it too, because it has no spell id; the reference stores its `0xffffffff` (`0x4d7b05`) and reads back 1, which the stock window's `> 1` test treats the same as 0. ([`src/script/trainer/mod.rs:871`](../crates/benilla-ui/src/script/trainer/mod.rs#L871))
|
||||
|
||||
Reference in New Issue
Block a user