//! ClassicAPI's `Container` namespace (`C_Container`): modern WoW's table/named-return shape over //! the same bag state the stock 1.12 verbs already expose (`crates/benilla-ui/src/script/container.rs`'s //! `Model.containers`) — almost entirely translation, not new game logic. Signatures are from the //! upstream repo's own `docs/API.md` (fetched 2026-10-05), not from memory. //! //! `GetContainerItemCharges` **is** built, and the row it used to carry here (the charge slot's //! identity being unknowable) was looking in the wrong place — see its own comment below. //! //! Not built, and why: //! `GetContainerItemRepairCost` — vanilla computes repair cost server-side only; the client never //! receives or derives it standalone. `GetContainerItemQuestInfo` — `ContainerSlot` tracks no //! quest-item flag. `IsContainerItemOpenable` — `ContainerSlot` tracks no lock/openable flag //! (`PickupContainerItem`'s lock bool is a *trade/pickup* lock, not a lockbox). All three return //! nil/false rather than guess. `SortBags`/`SortBankBags` are no-ops: 1.12 has no bag-sort feature //! and this client has no sort algorithm to run, only the preference toggles below, which nothing //! downstream reads yet. use mlua::{Lua, Table, Value}; use crate::script::{BagAutoStore, ContainerMove, Model}; /// Vanilla's Hearthstone, a single well-known item id unchanged since 1.12 (not a value that /// needs confirming against OctoWoW's own item data — it predates private servers). The corpus /// confirms it independently of memory: `pfQuest/db/enUS/items.lua:5642` is `[6948] = "Hearthstone"`, /// and it is the id pfQuest looks that row up by. Also published to Lua as the bare /// `HEARTHSTONE_ITEM_ID` global — see [`install`]. const HEARTHSTONE_ITEM_ID: u32 = 6948; fn item_id_at(model: &Model, bag: i64, slot: u32) -> Option { model .containers .get(&bag) .and_then(|c| c.slots.get(&slot)) .map(|s| s.item_id) .filter(|&id| id != 0) } /// Every `(bag, slot)` holding `item_id`, backpack and the four worn bags only (0..=4) unless /// told otherwise — `CalculateTotalNumberOfFreeBagSlots`'s own range, and hearthstone lookups'. fn find_item<'a>(model: &'a Model, bags: &'a [i64], item_id: u32) -> Option<(i64, u32)> { bags.iter().find_map(|&bag| { model.containers.get(&bag).and_then(|c| { c.slots .iter() .find(|(_, s)| s.item_id == item_id) .map(|(&slot, _)| (bag, slot)) }) }) } const CARRIED_BAGS: [i64; 5] = [0, 1, 2, 3, 4]; /// A bag's free-slot count: `num_slots` minus every slot key the app has pushed, resolved or /// still in flight — anything not a key at all is the genuinely empty slot this counts. fn free_slots(model: &Model, bag: i64) -> Vec { let Some(c) = model.containers.get(&bag) else { return Vec::new(); }; (1..=c.num_slots) .filter(|slot| !c.slots.contains_key(slot)) .collect() } pub(super) fn install(lua: &Lua) -> mlua::Result { // `HEARTHSTONE_ITEM_ID`: the bare global modern FrameXML declares beside // `C_Container.PlayerHasHearthstone`/`UseHearthstone` (the two verbs below), which is why it // installs here rather than in a constants file of its own. 1.12 has no such global — // `reference/1.12-globals.tsv` has no row for it — and the corpus assumes it exists: // // * `pfQuest/database.lua:2217-2241` — **the reason this is not cosmetic.** The file's last // block decides whether the server speaks the same locale as pfQuest's shipped databases // by comparing the Hearthstone's live name against `pfDB["items"][loc][HEARTHSTONE_ITEM_ID]`, // and `pfDatabase.localized = true` is set *inside* that comparison's `if`. With the global // nil, `C_Item.IsItemDataCachedByID(nil)` is false, `Item:CreateFromItemID(nil)` makes an // empty item whose `ContinueOnItemLoad` poll can never resolve, so `localized` stays nil // for the session — and pfQuest's `OnUpdate` returns at `quest.lua:352` on every frame // before it reaches `UpdateQuestlog` or `SearchQuests`. That is the whole addon's quest // tracker, map pins and minimap icons, off, with nothing logged: reading `t[nil]` is legal // in Lua and the never-firing callback raises nothing. // * `pfUI/libs/libbagsort.lua:19` and `pfUI/modules/itemcount.lua:16` compare a bag slot's // item id against it; a nil there is a silently-never-taken branch, not a raise. // // A `nil == nil` comparison is exactly why this went unseen for so long, so the value is // published once, from the one place that already held it. lua.globals() .set("HEARTHSTONE_ITEM_ID", HEARTHSTONE_ITEM_ID)?; let t = lua.create_table()?; t.set( "GetContainerItemID", lua.create_function(|lua, (bag, slot): (i64, u32)| { let model = lua.app_data_ref::().expect("model app_data"); Ok(item_id_at(&model, bag, slot).map(i64::from)) })?, )?; t.set( "HasContainerItem", lua.create_function(|lua, (bag, slot): (i64, u32)| { let model = lua.app_data_ref::().expect("model app_data"); Ok(item_id_at(&model, bag, slot).is_some()) })?, )?; t.set( "GetContainerItemInfo", lua.create_function(|lua, (bag, slot): (i64, u32)| { let model = lua.app_data_ref::().expect("model app_data"); let Some(s) = model.containers.get(&bag).and_then(|c| c.slots.get(&slot)) else { return Ok(Value::Nil); }; if s.item_id == 0 { return Ok(Value::Nil); } let info = lua.create_table()?; info.set("itemID", s.item_id)?; info.set("quantity", s.count)?; if let Some(q) = s.quality { info.set("quality", q)?; } info.set("isBound", s.already_bound)?; info.set("isReadable", s.readable)?; info.set("isLocked", s.locked)?; // No per-item "worthless" fact is tracked; false, not a guess either way. info.set("hasNoValue", false)?; info.set("isFiltered", false)?; info.set("noValue", false)?; if let Some(link) = &s.link { info.set("itemLink", link.as_str())?; info.set("hyperlink", link.as_str())?; } Ok(Value::Table(info)) })?, )?; // GetItemCooldown(itemID): scans the carried bags for the item, reusing the same // `container_cooldowns` the stock `GetContainerItemCooldown` reads; an item not carried, or // carried with no active cooldown, answers the reference's own "cold" triple. t.set( "GetItemCooldown", lua.create_function(|lua, item_id: u32| { let now: f64 = lua.globals().get("__benilla_now").unwrap_or(0.0); let model = lua.app_data_ref::().expect("model app_data"); let found = find_item(&model, &CARRIED_BAGS, item_id) .and_then(|(bag, slot)| model.container_cooldowns.get(&(bag, slot))); Ok(match found { Some(&(start, duration, enabled)) if start + duration > now || !enabled => { (start, duration, i32::from(enabled)) } _ => (0.0, 0.0, 1), }) })?, )?; t.set( "GetContainerItemDurability", lua.create_function(|lua, (bag, slot): (i64, u32)| { let model = lua.app_data_ref::().expect("model app_data"); let durability = model .containers .get(&bag) .and_then(|c| c.slots.get(&slot)) .and_then(|s| s.durability); Ok(match durability { Some((cur, max)) => ( Value::Integer(i64::from(cur)), Value::Integer(i64::from(max)), ), None => (Value::Nil, Value::Nil), }) })?, )?; t.set( "GetContainerNumFreeSlots", lua.create_function(|lua, bag: i64| { let model = lua.app_data_ref::().expect("model app_data"); Ok(free_slots(&model, bag).len()) })?, )?; t.set( "GetContainerFreeSlots", lua.create_function(|lua, bag: i64| { let model = lua.app_data_ref::().expect("model app_data"); lua.create_sequence_from(free_slots(&model, bag)) })?, )?; t.set( "CalculateTotalNumberOfFreeBagSlots", lua.create_function(|lua, ()| { let model = lua.app_data_ref::().expect("model app_data"); Ok(CARRIED_BAGS .iter() .map(|&bag| free_slots(&model, bag).len()) .sum::()) })?, )?; // Always nil: 1.12 has no quest-item tracking on a bag slot and no equipment-set feature at // all, so there is never a quest-item or a set to report — a real answer, not a stand-in. t.set( "GetContainerItemQuestInfo", lua.create_function(|_, (_, _): (i64, u32)| Ok(Value::Nil))?, )?; t.set( "GetContainerItemEquipmentSetInfo", lua.create_function(|_, (_, _): (i64, u32)| Ok(Value::Nil))?, )?; t.set( "GetContainerItemRepairCost", lua.create_function(|_, (_, _): (i64, u32)| Ok(Value::Nil))?, )?; // GetContainerItemCharges(bag, slot) -> uses left in the item's charge pool. // // **This reverses this file's earlier decision not to build it**, and the objection recorded // there was wrong about where the data lives rather than right-but-outweighed: it looked for // the charge slot's identity in `benilla-formats`' item template, which is DBC-shaped and has // no such column — but item templates do not come from a DBC here, they come over the wire. // `ItemInfo::spells` carries all five `ItemSpell` blocks with their own signed `SpellCharges` // (`benilla-protocol`'s `ItemSpellEntry`), so "which block is a consumable charge pool" is // answered by the block itself, and `ui_items/feed.rs`'s `charge_pool` was already picking it // out — the tooltip's "N Charges" line has been printed off that same choice all along. All // that was missing was carrying the *instance's* countdown of it onto `ContainerSlot`. // // Always a number, never nil, for any `(bag, slot)` — an item with no pool, an empty slot and // an absent bag all answer 0, the same shape `GetContainerNumFreeSlots` above takes for an // absent bag. 0 is true of each: there is no charge pool to have uses left in. It also has to // be a number, because pfUI's bag module compares the return with `> 1` unguarded // (`modules/bags.lua:452`), and the nil this used to answer raised there — which aborted // `CreateBags` partway through the backpack and so never reached its `CreateBags("bank")` // call, leaving `pfUI.bags[-1]` and `[5..11]` uncreated and the next `CheckFullUpdate` // indexing nil at `:210`. One nil, two reported faults. // // **What is chosen rather than confirmed**: "uses left" is the member's documented meaning, // but upstream's exact arithmetic is not in hand (`docs/ADDON_COMPAT.md`'s row on why — the // published `docs/API.md` is on `master`, not the `main` the old row cited, and the body of // the Item section could not be read). The other plausible candidate is `stackCount x usesPer // Item`, which differs from this only for a *stack* of charged items. The one installed call // site cannot tell them apart and that is checked, not assumed: pfUI reaches this verb only // inside `if count == 1`, where the two candidates coincide, and only ever tests `> 1`. t.set( "GetContainerItemCharges", lua.create_function(|lua, (bag, slot): (i64, u32)| { let model = lua.app_data_ref::().expect("model app_data"); Ok(model .containers .get(&bag) .and_then(|c| c.slots.get(&slot)) .map_or(0, |s| s.charges)) })?, )?; t.set( "IsContainerItemOpenable", lua.create_function(|_, (_, _): (i64, u32)| Ok(false))?, )?; t.set( "PlayerHasHearthstone", lua.create_function(|lua, ()| { let model = lua.app_data_ref::().expect("model app_data"); Ok(find_item(&model, &CARRIED_BAGS, HEARTHSTONE_ITEM_ID).is_some()) })?, )?; t.set( "UseHearthstone", lua.create_function(|lua, ()| { let mut model = lua.app_data_mut::().expect("model app_data"); let Some((bag, slot)) = find_item(&model, &CARRIED_BAGS, HEARTHSTONE_ITEM_ID) else { return Ok(false); }; model.container_uses.push((bag, slot)); Ok(true) })?, )?; // SwapItems/MoveItem: queued exactly as `PickupContainerItem`'s own place-onto-another-slot // leg does (`script/container.rs`'s `queue_move`), just without needing the cursor round // trip; both answer whether the source actually held an item to move. t.set( "SwapItems", lua.create_function( |lua, (src_bag, src_slot, dst_bag, dst_slot): (i64, u32, i64, u32)| { let mut model = lua.app_data_mut::().expect("model app_data"); if item_id_at(&model, src_bag, src_slot).is_none() { return Ok(false); } model.container_moves.push(ContainerMove { src_bag, src_slot, dst_bag, dst_slot, count: None, }); Ok(true) }, )?, )?; t.set( "MoveItem", lua.create_function( |lua, (src_bag, src_slot, dst_bag, dst_slot, count): (i64, u32, i64, u32, u32)| { let mut model = lua.app_data_mut::().expect("model app_data"); let Some(stack) = model .containers .get(&src_bag) .and_then(|c| c.slots.get(&src_slot)) .filter(|s| s.item_id != 0) .map(|s| s.count) else { return Ok(false); }; model.container_moves.push(ContainerMove { src_bag, src_slot, dst_bag, dst_slot, count: (count < stack).then_some(count), }); Ok(true) }, )?, )?; t.set( "AutoStoreItem", lua.create_function( |lua, (src_bag, src_slot, dst_bag): (i64, u32, Option)| { let mut model = lua.app_data_mut::().expect("model app_data"); if item_id_at(&model, src_bag, src_slot).is_none() { return Ok(()); } model.bag_autostores.push(BagAutoStore { src_bag, src_slot, dst_bag: dst_bag.unwrap_or(0), count: None, }); Ok(()) }, )?, )?; t.set("SortBags", lua.create_function(|_, ()| Ok(()))?)?; t.set("SortBankBags", lua.create_function(|_, ()| Ok(()))?)?; t.set( "GetSortBagsRightToLeft", lua.create_function(|lua, ()| { Ok(lua .app_data_ref::() .expect("model app_data") .bag_sort_right_to_left) })?, )?; t.set( "SetSortBagsRightToLeft", lua.create_function(|lua, enable: bool| { lua.app_data_mut::() .expect("model app_data") .bag_sort_right_to_left = enable; Ok(()) })?, )?; t.set( "GetBackpackAutosortDisabled", lua.create_function(|lua, ()| { Ok(lua .app_data_ref::() .expect("model app_data") .backpack_autosort_disabled) })?, )?; t.set( "SetBackpackAutosortDisabled", lua.create_function(|lua, disable: bool| { lua.app_data_mut::() .expect("model app_data") .backpack_autosort_disabled = disable; Ok(()) })?, )?; t.set( "GetBankAutosortDisabled", lua.create_function(|lua, ()| { Ok(lua .app_data_ref::() .expect("model app_data") .bank_autosort_disabled) })?, )?; t.set( "SetBankAutosortDisabled", lua.create_function(|lua, disable: bool| { lua.app_data_mut::() .expect("model app_data") .bank_autosort_disabled = disable; Ok(()) })?, )?; Ok(t) } #[cfg(test)] mod tests { use crate::script::{ContainerSlot, ContainerState, UiScript}; fn backpack_with(slot: u32, item_id: u32, count: u32) -> ContainerState { let mut slots = std::collections::HashMap::new(); slots.insert( slot, ContainerSlot { item_id, count, ..Default::default() }, ); ContainerState { name: Some("Backpack".into()), num_slots: 16, slots, } } #[test] fn basic_item_reads_wrap_the_stock_slot_data() { let mut s = UiScript::new().unwrap(); s.set_container(0, Some(backpack_with(1, 117, 5))); assert_eq!( s.eval::("return C_Container.GetContainerItemID(0, 1)") .unwrap(), 117 ); assert!(s .eval::("return C_Container.HasContainerItem(0, 1)") .unwrap()); assert!(!s .eval::("return C_Container.HasContainerItem(0, 2)") .unwrap()); assert_eq!( s.eval::("local info = C_Container.GetContainerItemInfo(0, 1) return info.itemID") .unwrap(), 117 ); assert_eq!( s.eval::( "local info = C_Container.GetContainerItemInfo(0, 1) return info.quantity" ) .unwrap(), 5 ); assert!(s .eval::("return C_Container.GetContainerItemInfo(0, 2) == nil") .unwrap()); } #[test] fn free_slots_exclude_every_known_key_resolved_or_not() { let mut s = UiScript::new().unwrap(); let mut state = backpack_with(1, 117, 5); state.num_slots = 4; state.slots.insert(3, ContainerSlot::default()); // in-flight, still "known" s.set_container(0, Some(state)); assert_eq!( s.eval::("return C_Container.GetContainerNumFreeSlots(0)") .unwrap(), 2, "slots 2 and 4 only" ); assert_eq!( s.eval::>("return C_Container.GetContainerFreeSlots(0)") .unwrap(), vec![2, 4] ); } #[test] fn hearthstone_lookup_scans_the_carried_bags() { let mut s = UiScript::new().unwrap(); assert!(!s .eval::("return C_Container.PlayerHasHearthstone()") .unwrap()); s.set_container(2, Some(backpack_with(3, 6948, 1))); assert!(s .eval::("return C_Container.PlayerHasHearthstone()") .unwrap()); assert!(s .eval::("return C_Container.UseHearthstone()") .unwrap()); assert_eq!(s.take_container_uses(), vec![(2, 3)]); } #[test] fn swap_and_move_queue_container_moves() { let mut s = UiScript::new().unwrap(); s.set_container(0, Some(backpack_with(1, 117, 5))); assert!(s .eval::("return C_Container.SwapItems(0, 1, 0, 2)") .unwrap()); assert_eq!( s.take_container_moves(), vec![crate::script::ContainerMove { src_bag: 0, src_slot: 1, dst_bag: 0, dst_slot: 2, count: None, }] ); assert!( !s.eval::("return C_Container.SwapItems(0, 9, 0, 2)") .unwrap(), "nothing in slot 9" ); assert!(s .eval::("return C_Container.MoveItem(0, 1, 0, 2, 3)") .unwrap()); assert_eq!( s.take_container_moves(), vec![crate::script::ContainerMove { src_bag: 0, src_slot: 1, dst_bag: 0, dst_slot: 2, count: Some(3), }], "partial, below the 5-stack" ); assert!(s .eval::("return C_Container.MoveItem(0, 1, 0, 2, 5)") .unwrap()); assert_eq!( s.take_container_moves(), vec![crate::script::ContainerMove { src_bag: 0, src_slot: 1, dst_bag: 0, dst_slot: 2, count: None, }], "the whole stack reads as a plain move, no split" ); } #[test] fn autostore_queues_when_something_is_there() { let mut s = UiScript::new().unwrap(); s.set_container(0, Some(backpack_with(1, 117, 5))); s.eval::<()>("C_Container.AutoStoreItem(0, 1)").unwrap(); s.eval::<()>("C_Container.AutoStoreItem(0, 9)").unwrap(); // empty, no-op assert_eq!( s.take_bag_autostores(), vec![crate::script::BagAutoStore { src_bag: 0, src_slot: 1, dst_bag: 0, count: None, }] ); } #[test] fn sort_toggles_round_trip_as_stored_preferences() { let s = UiScript::new().unwrap(); assert!(!s .eval::("return C_Container.GetSortBagsRightToLeft()") .unwrap()); s.eval::<()>("C_Container.SetSortBagsRightToLeft(true)") .unwrap(); assert!(s .eval::("return C_Container.GetSortBagsRightToLeft()") .unwrap()); s.eval::<()>("C_Container.SetBackpackAutosortDisabled(true)") .unwrap(); assert!(s .eval::("return C_Container.GetBackpackAutosortDisabled()") .unwrap()); s.eval::<()>("C_Container.SetBankAutosortDisabled(true)") .unwrap(); assert!(s .eval::("return C_Container.GetBankAutosortDisabled()") .unwrap()); // No-ops that must not raise. s.eval::<()>("C_Container.SortBags() C_Container.SortBankBags()") .unwrap(); } #[test] fn durability_and_quest_and_equipment_set_info_are_honest_about_what_is_not_tracked() { let mut s = UiScript::new().unwrap(); let mut state = backpack_with(1, 117, 1); state.slots.get_mut(&1).unwrap().durability = Some((40, 100)); s.set_container(0, Some(state)); assert_eq!( s.eval::<(i64, i64)>("return C_Container.GetContainerItemDurability(0, 1)") .unwrap(), (40, 100) ); assert!(s .eval::("return C_Container.GetContainerItemQuestInfo(0, 1) == nil") .unwrap()); assert!(s .eval::("return C_Container.GetContainerItemEquipmentSetInfo(0, 1) == nil") .unwrap()); assert!(s .eval::("return C_Container.GetContainerItemRepairCost(0, 1) == nil") .unwrap()); assert!(!s .eval::("return C_Container.IsContainerItemOpenable(0, 1)") .unwrap()); } /// pfUI's bag module reads this one unguarded — `local charges = ...GetContainerItemCharges( /// bag, slot) if charges > 1 then` (`modules/bags.lua:451-452`) — so the type matters as much /// as the number: a nil raises "attempt to compare number with nil" *inside* `CreateBags`, /// which then never reaches its own `CreateBags("bank")` call and leaves `pfUI.bags[-1]` and /// `[5..11]` absent for the next `CheckFullUpdate` to index at `:210`. The body below is that /// loop, transcribed, so the test fails the same way the addon did. #[test] fn container_item_charges_answers_a_number_for_every_slot_so_pfuis_bag_loop_cannot_raise() { let mut s = UiScript::new().unwrap(); let mut state = backpack_with(1, 117, 1); // A five-charge pool with two uses spent: `ContainerSlot::charges` is the *instance's* // remaining count, so this is 3, not the template's 5. state.slots.get_mut(&1).unwrap().charges = 3; s.set_container(0, Some(state)); assert_eq!( s.eval::("return C_Container.GetContainerItemCharges(0, 1)") .unwrap(), 3, "the uses left on the item in that slot" ); // An item with no pool, an empty slot and an absent bag: 0, and a number in every case. for call in [ "C_Container.GetContainerItemCharges(0, 2)", "C_Container.GetContainerItemCharges(5, 1)", ] { assert_eq!( s.eval::(&format!("return type({call})")).unwrap(), "number", "{call} must answer a number: pfUI compares it with `> 1` unguarded" ); assert_eq!(s.eval::(&format!("return {call}")).unwrap(), 0); } // `modules/bags.lua:446-455`, transcribed: the block that raised. s.run( r#" local bag, slot = 0, 1 local count = 1 if count == 1 then local charges = C_Container.GetContainerItemCharges(bag, slot) if charges > 1 then count = charges end end assert(count == 3, "pfUI shows the charge count in the stack corner") -- and the same block over every bag the module walks, including the ones with no -- container pushed at all, which is what `for bag = -2,11` reaches. for bag = -2, 11 do for slot = 1, 4 do local c = C_Container.GetContainerItemCharges(bag, slot) if c > 1 then count = c end end end "#, ) .expect("pfUI's charge block must not raise for any bag in -2..11"); } #[test] fn item_cooldown_scans_carried_bags_by_item_id() { let mut s = UiScript::new().unwrap(); s.tick(100.0); let mut state = backpack_with(1, 117, 1); state.slots.get_mut(&1).unwrap().cooldown = Some((96_000, 10_000, true)); s.set_container(0, Some(state)); assert!(s .eval::( "local st, d, e = C_Container.GetItemCooldown(117) return st == 96 and d == 10 and e == 1" ) .unwrap()); assert!(s .eval::( "local st, d, e = C_Container.GetItemCooldown(9999) return st == 0 and d == 0 and e == 1" ) .unwrap()); } /// **pfQuest's locale check, transcribed** (`database.lua:2215-2246`), the last block of the /// last file of its `` — and the single gate the whole addon runs behind: /// `quest.lua:352` is `if not pfDatabase.localized then return end`, the first statement of /// `pfQuest`'s `OnUpdate`, so until this block sets the flag there is no `UpdateQuestlog`, no /// `SearchQuests`, no queue drain, no map node and no tracker row. /// /// The flag is assigned *inside* `if name and name ~= "" and pfDB["items"][loc] and /// pfDB["items"][loc][HEARTHSTONE_ITEM_ID]`, which makes the global load-bearing twice over: as /// the argument that decides whether the cached branch is even taken, and as the *key* into /// pfQuest's own shipped locale table. So this fails against a nil global (the empty /// `Item:CreateFromItemID(nil)` whose poll can never resolve) and against a wrong id (the key /// misses and the `if` is never entered) alike — and neither failure raises, which is exactly /// why the symptom was an addon that drew nothing with a clean error log. #[test] fn pfquests_locale_check_sets_its_run_gate_from_the_hearthstone_the_bags_hold() { let mut s = UiScript::new().unwrap(); s.set_container(0, Some(backpack_with(1, 6948, 1))); // The bag push is what makes the app query the template (`ui_items/feed.rs`'s // `items.template(entry, …)` per slot), so by the time an addon loads the answer is in the // store and `IsItemDataCachedByID` takes the synchronous branch — as it does here. s.set_item_template( 6948, crate::script::ItemTemplateView { name: "Hearthstone".into(), quality: 1, ..Default::default() }, ); s.run( r#" -- pfQuest's own locale table, reduced to the row this check reads -- (`db/enUS/items.lua:5642`), and the two objects the block writes through local loc = GetLocale() pfDB = { items = { [loc] = { [6948] = "Hearthstone" } } } pfDatabase = {} function pfDatabase:BuildNameIndex() self.name_index_built = true end function pfDatabase:BuildStaticRejectSet() self.reject_set_built = true end local function RunNameCheck(name) if name and name ~= "" and pfDB["items"][loc] and pfDB["items"][loc][HEARTHSTONE_ITEM_ID] then if not strfind(name, pfDB["items"][loc][HEARTHSTONE_ITEM_ID], 1) then -- the unlocalized-server arm: swaps the locale tables, not exercised here pfDatabase.swapped = true end pfDatabase.localized = true pfDatabase:BuildNameIndex() pfDatabase:BuildStaticRejectSet() end end if C_Item.IsItemDataCachedByID(HEARTHSTONE_ITEM_ID) then RunNameCheck(C_Item.GetItemNameByID(HEARTHSTONE_ITEM_ID)) else local item = Item:CreateFromItemID(HEARTHSTONE_ITEM_ID) item:ContinueOnItemLoad(function() RunNameCheck(item:GetItemName()) end) end "#, ) .expect("pfQuest's locale check must not raise"); assert_eq!( s.eval::>("return pfDatabase.localized") .unwrap(), Some(true), "the run gate every pfQuest feature sits behind (`quest.lua:352`) must be open" ); assert_eq!( s.eval::>("return pfDatabase.swapped").unwrap(), None, "an enUS server matching the enUS table must not take the locale-swap arm" ); assert_eq!( s.eval::>("return pfDatabase.name_index_built") .unwrap(), Some(true), "and the derived indexes the same block owns got rebuilt" ); } }