diff --git a/crates/benilla-app/src/ui_script/lifecycle.rs b/crates/benilla-app/src/ui_script/lifecycle.rs index 31649ad4..3ce02e6b 100644 --- a/crates/benilla-app/src/ui_script/lifecycle.rs +++ b/crates/benilla-app/src/ui_script/lifecycle.rs @@ -38,6 +38,8 @@ fn install_boot_vm(world: &mut World) { install_addon_asset_resolvers(world, &mut script); install_spell_catalog_probe(world, &mut script); install_item_icon_probe(world, &mut script); + install_item_set_probe(world, &mut script); + install_area_trigger_probe(world, &mut script); install_spell_modifier_probe(world, &mut script); install_custom_files(&mut script); // nampower's `EncryptPassword`: a pure function over the machine-local key file. @@ -203,6 +205,77 @@ pub(crate) fn install_spell_probe(script: &mut UiScript, catalog: &benilla_forma /// icon is the only column either verb wants, the rest of an `ItemDisplay` is model and texture /// paths for the renderer, and a VM build is rare (boot, world entry, `/reload`). Rows without an /// icon are left out, so a hit is always a path. +/// `ItemSet.dbc` behind ClassicAPI's `C_Item.GetItemSetInfo`. The same catalog the item +/// tooltip's set block reads (`ui_items::ItemSets`), handed over by id rather than pushed: four +/// hundred rows of seventeen item columns is not a per-VM copy for a verb nothing calls at boot. +fn install_item_set_probe(world: &World, script: &mut UiScript) { + let Some(sets) = world.get_resource::() else { + return; + }; + // Copied rather than shared by refcount: the catalog is small, the closure outlives the + // system run, and `ItemSetCatalog` has no shared-handle accessor the way `SpellCatalog` does. + let rows: std::collections::HashMap = sets + .0 + .iter() + .map(|(id, set)| { + ( + id, + benilla_ui::script::ItemSetLookup { + name: set.name.clone(), + // The empty item columns are dropped here, so `items[1]` is a real piece — the + // same rule the spell probe applies to the reagent columns. + items: set.items.iter().copied().filter(|i| *i != 0).collect(), + // The DBC stores `(required count, spell id)`; the reader's documented pair is + // `{ spellID, threshold }`, so the swap happens once, here. + bonuses: set + .bonuses + .iter() + .filter(|(count, spell)| *spell != 0 && *count != 0) + .map(|(count, spell)| (*spell, *count)) + .collect(), + required_skill: set.required_skill, + required_skill_rank: set.required_skill_rank, + }, + ) + }) + .collect(); + script.set_item_set_probe(Box::new(move |id| rows.get(&id).cloned())); +} + +/// `AreaTrigger.dbc` behind ClassicAPI's `C_Map.GetAreaTriggerInfo`, each row's centre resolved to +/// a zone and a map percentage by [`crate::ui_world_map::WorldMapUiData::zone_at`] — the same law +/// the map window's hover uses, so a trigger lands where the map would put it. +/// +/// Resolved **here**, once per VM build, rather than per call: the resolution needs the +/// `WorldMapArea` rects, which do not cross the seam, and the whole table is a few thousand rows. +/// A world with no map data (or no trigger DBC) installs no probe, and the verb answers nothing. +fn install_area_trigger_probe(world: &World, script: &mut UiScript) { + let Some(triggers) = world.get_resource::() else { + return; + }; + let maps = world.get_resource::(); + let rows: std::collections::HashMap = triggers + .0 + .iter() + .map(|row| { + ( + row.id, + benilla_ui::script::AreaTriggerLookup { + map_id: row.map_id, + position: row.position, + radius: row.radius, + // `None` for a trigger on no mapped zone — an instance interior, or a point + // outside every `WorldMapArea` rect. pfQuest tests for exactly that before + // using the pair. + zone: maps + .and_then(|m| m.zone_at(row.map_id, row.position[0], row.position[1])), + }, + ) + }) + .collect(); + script.set_area_trigger_probe(Box::new(move |id| rows.get(&id).copied())); +} + fn install_item_icon_probe(world: &World, script: &mut UiScript) { let Some(displays) = world.get_resource::() else { return; diff --git a/crates/benilla-app/src/ui_world_map.rs b/crates/benilla-app/src/ui_world_map.rs index 807839ca..0e444bd5 100644 --- a/crates/benilla-app/src/ui_world_map.rs +++ b/crates/benilla-app/src/ui_world_map.rs @@ -279,6 +279,27 @@ pub(crate) struct WorldMapCatalog(pub(crate) Vec); /// [`crate::ui_script::load_ingame_ui_on_world_entry`]: addons read `GetMapContinents` and /// `GetMapZones` at file scope (Astrolabe builds its zone table there), and the reference holds /// this DBC data from load. +impl WorldMapUiData { + /// The zone a world point falls in on `map_id`, as `(AreaTable id, mapX, mapY)` with the two + /// fractions in **percent** — the shape ClassicAPI's `C_Map.GetAreaTriggerInfo` answers and + /// pfQuest feeds straight into `pfMap:AddNode` (`database.lua:805-806`), whose coordinates are + /// 0-100. + /// + /// Containment is [`map_proj::zone_uv`]'s own verdict rather than a fresh rect compare, so a + /// point lands in the same zone the map window's hover would name it in — including its one + /// quirk, that a point exactly on the east or south edge reads as outside (the law's `1 − …` + /// hits 0 there, and the function reports `(0, 0)` for anything off the rect). The first + /// matching zone wins; 1.12's zone rects do not overlap within a continent. + pub(crate) fn zone_at(&self, map_id: u32, wx: f32, wy: f32) -> Option<(u32, f32, f32)> { + let cont = self.continents.iter().find(|c| c.map_id == map_id)?; + cont.zones.iter().find_map(|z| { + let (u, v) = map_proj::zone_uv(z.rect, wx, wy); + // `(0, 0)` is this law's "not on this map", which is why it cannot be a hit. + ((u, v) != (0.0, 0.0)).then_some((z.area_id, u * 100.0, v * 100.0)) + }) + } +} + pub(crate) fn seed_world_map_catalog(world: &mut World, script: &mut UiScript) { if !world.contains_resource::() { let Some((views, data)) = build_catalog_from_world(world) else { diff --git a/crates/benilla-formats/src/area_trigger.rs b/crates/benilla-formats/src/area_trigger.rs index 449d9391..787872b1 100644 --- a/crates/benilla-formats/src/area_trigger.rs +++ b/crates/benilla-formats/src/area_trigger.rs @@ -85,6 +85,13 @@ impl AreaTriggerCatalog { self.by_map.values().flatten().find(|t| t.id == id) } + /// Every row, in no particular order. For the host probe behind ClassicAPI's + /// `C_Map.GetAreaTriggerInfo`, which resolves the whole table once per VM build rather than + /// walking the buckets per call the way [`Self::get`] does. + pub fn iter(&self) -> impl Iterator { + self.by_map.values().flatten() + } + pub fn len(&self) -> usize { self.len } diff --git a/crates/benilla-formats/src/itemsets.rs b/crates/benilla-formats/src/itemsets.rs index 5e18f742..4ab6aeee 100644 --- a/crates/benilla-formats/src/itemsets.rs +++ b/crates/benilla-formats/src/itemsets.rs @@ -34,6 +34,12 @@ impl ItemSetCatalog { self.sets.get(&id) } + /// Every set as `(id, row)`. For the host probe behind ClassicAPI's `C_Item.GetItemSetInfo`, + /// which needs the whole table once rather than one row at a time. + pub fn iter(&self) -> impl Iterator { + self.sets.iter().map(|(id, set)| (*id, set)) + } + pub fn len(&self) -> usize { self.sets.len() } diff --git a/crates/benilla-ui/src/script/compat/classic_api/item.rs b/crates/benilla-ui/src/script/compat/classic_api/item.rs index a3d7bc1e..e72bfbc1 100644 --- a/crates/benilla-ui/src/script/compat/classic_api/item.rs +++ b/crates/benilla-ui/src/script/compat/classic_api/item.rs @@ -761,6 +761,52 @@ pub(super) fn install(lua: &Lua) -> mlua::Result { })?, )?; + // `GetItemSetInfo(setID)` -> the `ItemSet.dbc` row as a table, nil for a set id nothing + // carries. The shape is the caller's own documentation (`SuperCleveRoidMacros/ + // ClassicAPI.lua:171-174`): `{ setID, name, requiredSkill, requiredSkillRank, items = {...}, + // bonuses = { { spellID, threshold }, ... } }` — note that the bonus pair is spell-then-count + // where the DBC stores count-then-spell, so the order here is the reader's and not the file's. + // + // Through a host probe, like `C_Spell`'s: `ItemSet.dbc` is the app's catalog + // (`ui_items::ItemSets`, which the item tooltip's set block already reads), and four hundred + // rows of seventeen item columns is not a table to push across the seam for a verb nothing + // calls at boot. 1.12 answers none of this in Lua — the tooltip prints the set block C-side + // and `GetItemInfo` reports no set at all, so an addon cannot tell which pieces complete one. + t.set( + "GetItemSetInfo", + lua.create_function(|lua, set_id: Option| { + let found = { + let model = lua.app_data_ref::().expect("model app_data"); + let probe = model.item_set_probe.as_ref(); + set_id + .filter(|id| *id != 0) + .and_then(|id| probe.and_then(|p| p(id)).map(|set| (id, set))) + }; + let Some((id, set)) = found else { + return Ok(Value::Nil); + }; + let out = lua.create_table()?; + out.set("setID", id)?; + out.set("name", set.name)?; + out.set("requiredSkill", set.required_skill)?; + out.set("requiredSkillRank", set.required_skill_rank)?; + let items = lua.create_table()?; + for (i, item) in (1..).zip(set.items) { + items.set(i, item)?; + } + out.set("items", items)?; + let bonuses = lua.create_table()?; + for (i, (spell, threshold)) in (1..).zip(set.bonuses) { + let pair = lua.create_table()?; + pair.set(1, spell)?; + pair.set(2, threshold)?; + bonuses.set(i, pair)?; + } + out.set("bonuses", bonuses)?; + Ok(Value::Table(out)) + })?, + )?; + // `GetWeaponEnchantInfo()`: the namespaced spelling of 1.12's own global // (`script/weapon_enchant.rs`), delegated whole — six values, two triples, main hand then // off hand. @@ -1488,4 +1534,41 @@ mod tests { (1, 3, 0) ); } + /// `GetItemSetInfo` over a probe, in the shape its one caller documents + /// (`SuperCleveRoidMacros/ClassicAPI.lua:171-174`) — including the bonus pair being + /// `{ spellID, threshold }`, which is the reverse of the DBC's own column order. + #[test] + fn the_item_set_table_is_the_shape_its_caller_documents() { + let mut s = UiScript::new().unwrap(); + s.set_item_set_probe(Box::new(|id| { + (id == 181).then(|| crate::script::ItemSetLookup { + name: "Lightforge Armor".into(), + items: vec![16729, 16730, 16731], + // Already swapped by the host: spell then count. + bonuses: vec![(13664, 3), (13665, 5)], + required_skill: 0, + required_skill_rank: 0, + }) + })); + assert!(s + .eval::( + r#"local set = C_Item.GetItemSetInfo(181) + return set.setID == 181 + and set.name == "Lightforge Armor" + and table.getn(set.items) == 3 and set.items[1] == 16729 + and table.getn(set.bonuses) == 2 + and set.bonuses[1][1] == 13664 and set.bonuses[1][2] == 3 + and set.bonuses[2][2] == 5 + and set.requiredSkill == 0"# + ) + .unwrap()); + assert!( + s.eval::("return C_Item.GetItemSetInfo(999) == nil") + .unwrap(), + "a set id nothing carries is nil, not an empty set" + ); + assert!(s + .eval::("return C_Item.GetItemSetInfo() == nil") + .unwrap()); + } } diff --git a/crates/benilla-ui/src/script/compat/classic_api/map.rs b/crates/benilla-ui/src/script/compat/classic_api/map.rs index 76f74be2..09c88d28 100644 --- a/crates/benilla-ui/src/script/compat/classic_api/map.rs +++ b/crates/benilla-ui/src/script/compat/classic_api/map.rs @@ -466,6 +466,51 @@ pub(super) fn install(lua: &Lua) -> mlua::Result
{ })?, )?; + // `GetAreaTriggerInfo(triggerID)` -> the trigger's geometry and where it is on the map, or + // nil for an id `AreaTrigger.dbc` does not carry. + // + // **pfQuest is the reader, and it replaces a shipped coordinate table with this**: its own + // comment says it "reads the trigger's geometry live from the client's AreaTrigger.dbc via + // ClassicAPI instead of a shipped coordinate table" (`database.lua:781-782`), and the three + // fields it takes — `areaID`, `mapX`, `mapY` — go straight into `pfMap:AddNode`, whose + // coordinates are 0-100. So the percentages are the measured shape, from the consumer. + // + // `areaID`/`mapX`/`mapY` are **absent together** when the centre resolves to no zone rect (an + // instance interior, or a trigger outside every `WorldMapArea`), which is exactly the case + // pfQuest tests for before using them. The geometry fields are always there, since they are + // the DBC row itself. + // + // Resolution is the host's, through the same `zone_uv` law the map window's own hover uses + // (`ui_world_map.rs`'s `WorldMapUiData::zone_at`), so a trigger lands in the zone the map + // would name for that point rather than in one this file decided on. + t.set( + "GetAreaTriggerInfo", + lua.create_function(|lua, trigger_id: Option| { + let found = { + let model = lua.app_data_ref::().expect("model app_data"); + let probe = model.area_trigger_probe.as_ref(); + trigger_id + .filter(|id| *id != 0) + .and_then(|id| probe.and_then(|p| p(id))) + }; + let Some(row) = found else { + return Ok(Value::Nil); + }; + let out = lua.create_table()?; + out.set("mapID", row.map_id)?; + out.set("x", f64::from(row.position[0]))?; + out.set("y", f64::from(row.position[1]))?; + out.set("z", f64::from(row.position[2]))?; + out.set("radius", f64::from(row.radius))?; + if let Some((area_id, map_x, map_y)) = row.zone { + out.set("areaID", area_id)?; + out.set("mapX", f64::from(map_x))?; + out.set("mapY", f64::from(map_y))?; + } + Ok(Value::Table(out)) + })?, + )?; + Ok(t) } @@ -1048,4 +1093,58 @@ mod tests { "the second read of the same row re-measures nothing" ); } + /// `GetAreaTriggerInfo` in the shape pfQuest reads (`database.lua:789-806`): the three fields + /// it tests for, in percent, and absent **together** when the centre resolves to no zone. + #[test] + fn the_area_trigger_table_carries_what_pfquest_tests_for() { + let mut s = UiScript::new().unwrap(); + s.set_area_trigger_probe(Box::new(|id| match id { + // A trigger on a mapped zone: resolved. + 4128 => Some(crate::script::AreaTriggerLookup { + map_id: 0, + position: [-8900.0, -130.0, 80.0], + radius: 7.0, + zone: Some((1519, 48.5, 62.25)), + }), + // One inside an instance, where no `WorldMapArea` rect holds it. + 100 => Some(crate::script::AreaTriggerLookup { + map_id: 33, + position: [0.0, 0.0, 0.0], + radius: 0.0, + zone: None, + }), + _ => None, + })); + + // pfQuest's own guard and reads, transcribed. + assert!(s + .eval::( + r#"local t = C_Map.GetAreaTriggerInfo(4128) + if not t or not t.areaID or not t.mapX or not t.mapY then return false end + return t.areaID == 1519 and t.mapX == 48.5 and t.mapY == 62.25"# + ) + .unwrap()); + // The geometry is there either way, since it is the DBC row itself. + assert!(s + .eval::( + r#"local t = C_Map.GetAreaTriggerInfo(4128) + return t.mapID == 0 and t.radius == 7 and t.x == -8900"# + ) + .unwrap()); + // An unresolvable centre drops the three together — which is what that guard is for. + assert!(s + .eval::( + r#"local t = C_Map.GetAreaTriggerInfo(100) + return t ~= nil and t.areaID == nil and t.mapX == nil and t.mapY == nil + and t.mapID == 33"# + ) + .unwrap()); + // An id the DBC does not carry is nil, not an empty table. + assert!(s + .eval::("return C_Map.GetAreaTriggerInfo(999999) == nil") + .unwrap()); + assert!(s + .eval::("return C_Map.GetAreaTriggerInfo() == nil") + .unwrap()); + } } diff --git a/crates/benilla-ui/src/script/mod.rs b/crates/benilla-ui/src/script/mod.rs index b500cdb1..e18684d1 100644 --- a/crates/benilla-ui/src/script/mod.rs +++ b/crates/benilla-ui/src/script/mod.rs @@ -240,6 +240,11 @@ pub use mail::{MailInboxRow, MailInvoice, MailSendRequest, MailState, Stationery pub use measure::TextMeasure; pub use merchant::{ItemStatsHead, MerchantItem, MerchantState}; pub(crate) use minimap::apply_model_attrs as apply_minimap_model_attrs; +pub use model::{ + AreaTriggerLookup, AreaTriggerProbe, FontProbe, ItemIconProbe, ItemSetLookup, ItemSetProbe, + SpellDbcExtras, SpellIconProbe, SpellLookup, SpellMechanicProbe, SpellProbe, SpellUsable, + TextureProbe, TextureSizeProbe, +}; pub use model::{ BlipOverrideKey, BlipOverrideRequest, ClassInfo, CreatureModelProbe, CreatureModelView, RaceInfo, @@ -249,10 +254,6 @@ pub use model::{ QueueRequest, RecValue, SelfCast, SelfMovement, ServerClock, SpellModifierProbe, SpellRangeProbe, SpellRecFieldProbe, SpellRecordProbe, }; -pub use model::{ - FontProbe, ItemIconProbe, SpellDbcExtras, SpellIconProbe, SpellLookup, SpellMechanicProbe, - SpellProbe, SpellUsable, TextureProbe, TextureSizeProbe, -}; pub(crate) use model::{Model, PendingTimer}; pub use party::{PartyMemberInfo, PartyRequest, PartyState, RaidMemberInfo, SavedInstanceInfo}; pub use pet::{PetActionView, PetStats}; diff --git a/crates/benilla-ui/src/script/model.rs b/crates/benilla-ui/src/script/model.rs index 034f90af..363100ec 100644 --- a/crates/benilla-ui/src/script/model.rs +++ b/crates/benilla-ui/src/script/model.rs @@ -79,6 +79,48 @@ pub struct SpellLookup { /// back to the spellbook. pub type SpellProbe = Box Option>; +/// One `ItemSet.dbc` row, for `C_Item.GetItemSetInfo`: the host's catalog answers by set id, since +/// `ItemSet.dbc` is the app's (`benilla-app`'s `ui_items::ItemSets`) and 400-odd rows of ten items +/// each is not something to push across the seam for a verb nothing calls at boot. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ItemSetLookup { + /// `ItemSet.dbc`'s localized `Name`. + pub name: String, + /// The member item ids, empty rows dropped — so `items[1]` is a real item and a set with + /// three pieces has three entries, not seventeen with holes. + pub items: Vec, + /// `(spell id, required equipped count)` per bonus, in stored slot order. **That order is the + /// caller's**: `SuperCleveRoidMacros/ClassicAPI.lua:171` documents the pair as + /// `{ spellID, threshold }`, where the DBC's own layout is the other way round. + pub bonuses: Vec<(u32, u32)>, + pub required_skill: u32, + pub required_skill_rank: u32, +} + +/// `ItemSet.dbc` by set id. +pub type ItemSetProbe = Box Option>; + +/// One `AreaTrigger.dbc` row with its position already resolved to a zone, for +/// `C_Map.GetAreaTriggerInfo`. The resolution is the host's: it needs the `WorldMapArea` rects +/// and the same `zone_uv` law the map window's hover uses, neither of which crosses the seam. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct AreaTriggerLookup { + /// `Map.dbc` id the volume lives on. + pub map_id: u32, + /// Centre, WoW world space. + pub position: [f32; 3], + /// Sphere radius in yards; `0` makes the row an oriented box. + pub radius: f32, + /// The `AreaTable` id of the zone the centre falls in, and the point as **percent** across + /// that zone's map. `None` when the centre resolves to no zone rect — an instance interior, + /// or a trigger outside every `WorldMapArea` — which is exactly the case pfQuest tests for + /// (`if not trigger.areaID or not trigger.mapX`). + pub zone: Option<(u32, f32, f32)>, +} + +/// `AreaTrigger.dbc` by trigger id, zone-resolved. +pub type AreaTriggerProbe = Box Option>; + /// The host's answer for an `ItemDisplayInfo` row id: its inventory icon as an extensionless /// `Interface\Icons\…` path. Keyed on the **display** id, not the item id, because that is the /// column an item template carries and the one the reference's own vendor-cursor path resolves @@ -552,6 +594,10 @@ pub(crate) struct Model { /// `Spell.dbc` by id for `C_Spell`'s getters, past the player's own book; `None` restricts /// them to the book, which is all a VM with no host can see. pub(crate) spell_probe: Option, + /// `ItemSet.dbc`, for `C_Item.GetItemSetInfo`. + pub(crate) item_set_probe: Option, + /// `AreaTrigger.dbc`, for `C_Map.GetAreaTriggerInfo`. + pub(crate) area_trigger_probe: Option, pub(crate) spell_mechanic_probe: Option, /// Saved-variables folders, one `.lua` each: the account's, then this character's. pub(crate) addons_saved_account: Option, @@ -1855,6 +1901,8 @@ impl Model { measurer: None, texture_probe: None, spell_probe: None, + item_set_probe: None, + area_trigger_probe: None, item_icon_probe: None, spell_modifier_probe: None, spell_rec_field_probe: None, diff --git a/crates/benilla-ui/src/script/spellbook.rs b/crates/benilla-ui/src/script/spellbook.rs index 85d41519..09d14ca0 100644 --- a/crates/benilla-ui/src/script/spellbook.rs +++ b/crates/benilla-ui/src/script/spellbook.rs @@ -204,6 +204,13 @@ impl super::UiScript { self.model_mut().spell_probe = Some(probe); } + /// The host's `ItemSet.dbc`, for `C_Item.GetItemSetInfo`. Installed on every VM build like the + /// spell probe above; a VM without one answers nothing, which is what an unknown set id + /// answers anyway. + pub fn set_item_set_probe(&mut self, probe: crate::script::ItemSetProbe) { + self.model_mut().item_set_probe = Some(probe); + } + /// Install the `Spell.dbc` mechanic lookup; see [`crate::script::SpellMechanicProbe`]. pub fn set_spell_mechanic_probe(&mut self, probe: crate::script::SpellMechanicProbe) { self.model_mut().spell_mechanic_probe = Some(probe); diff --git a/crates/benilla-ui/src/script/tests/reference_surface.rs b/crates/benilla-ui/src/script/tests/reference_surface.rs index b0a5b697..ee403a3c 100644 --- a/crates/benilla-ui/src/script/tests/reference_surface.rs +++ b/crates/benilla-ui/src/script/tests/reference_surface.rs @@ -330,6 +330,10 @@ fn allowed_beyond_1_12() -> HashSet<&'static str> { // 1.12 prints in a tooltip and returns from nothing, so no addon can compare two pieces // of gear; `C_Item.RequestLoadItemDataByID` is the modern async item load over the // ask-once queue the item mixin already drives. + // `C_Item.GetItemSetInfo` and `C_Map.GetAreaTriggerInfo` ride host probes over two DBCs + // 1.12 reads C-side and publishes nowhere: the tooltip prints a set block and + // `GetItemInfo` reports no set at all, and `AreaTrigger.dbc` reaches Lua through nothing — + // which is why pfQuest shipped a coordinate table instead and now reads the file. // ClassicAPI's detection pair (script/compat/classic_api/mod.rs): pfUI gates its whole // load on `CLASSIC_API_VERSION`, so this one is load-bearing, not cosmetic. "CLASSIC_API_VERSION", diff --git a/crates/benilla-ui/src/script/worldmap.rs b/crates/benilla-ui/src/script/worldmap.rs index ff591564..998d865d 100644 --- a/crates/benilla-ui/src/script/worldmap.rs +++ b/crates/benilla-ui/src/script/worldmap.rs @@ -393,6 +393,13 @@ fn revealed_overlays(state: &WorldMapState) -> Vec<&WorldMapOverlayView> { impl super::UiScript { /// Pushes the catalog at startup, queueing `WORLD_MAP_UPDATE` so an open map repaints. + /// The host's `AreaTrigger.dbc`, zone-resolved, for `C_Map.GetAreaTriggerInfo`. Installed on + /// every VM build like the spell and item-set probes; a VM without one answers nothing, which + /// is what an unknown trigger id answers anyway. + pub fn set_area_trigger_probe(&mut self, probe: crate::script::AreaTriggerProbe) { + self.model_mut().area_trigger_probe = Some(probe); + } + pub fn set_world_map_catalog(&mut self, continents: Vec) { let n = continents.len(); { diff --git a/docs/ADDON_COMPAT.md b/docs/ADDON_COMPAT.md index a18caf2d..3c9932e8 100644 --- a/docs/ADDON_COMPAT.md +++ b/docs/ADDON_COMPAT.md @@ -228,7 +228,7 @@ separately, so "cannot say" has to be distinguishable from "no". | `EventRegistry`, `CallbackRegistryMixin:RegisterFrameEvent`/`RegisterFrameEventAndCallback` | done | `compat/classic_api/callback_registry.rs`. The bridge from real frame events into the callback bus; pfUI's new-item tracker is `EventRegistry:RegisterFrameEventAndCallback("BAG_NEW_ITEMS_UPDATED", ...)` (`modules/newitem.lua:60`). 1.12's `OnEvent` takes **no parameters** — the handler reads the globals `event`/`arg1..arg9` with `this` as the frame — so the forwarder cannot be upstream's `function(_, event, ...)`. It is defined at **chunk scope** on purpose: written as a closure inside `RegisterFrameEvent` it would capture that function's `event` parameter and shadow the per-fire global, making the frame report every event under the name of whichever registered first. A two-event test catches exactly that and was verified to fail against the closure version (`left: (1, 0)` — the wrong callback fired) | | `GameTooltip_Hide` | done | `compat/classic_api/tooltip.rs`. A later-client FrameXML helper: `reference/1.12-globals.tsv` lists exactly six `GameTooltip_*` functions and this is not among them. Its purpose is to be passed *by name* as an `OnLeave` handler, which is all five of its call sites in the installed set — and nil there is not a no-op, it raised inside `HookScript` and took the rest of the module with it (`modules/friendnotes.lua:90`). Checked at the same time: `SetItemButtonTexture`/`SetItemButtonCount`, also called by the addons, **are** genuine 1.12 FrameXML globals and need nothing | | `C_Spell.GetSpellReagents` | done | `compat/classic_api/spell.rs`, over new `SpellLookup::reagents` — `Spell.dbc`'s `Reagent[8]`/`ReagentCount[8]`, which `benilla-formats` already parses (`spells/display.rs:122`, with `catalog_tests.rs` pinning Slow Fall's `(17056, 1)`). Empty columns are dropped at the feed, so `reagents[1]` is the first *real* reagent. Returns an empty table for a spell that needs nothing and **nil** for an unknown id — genuinely different answers, both of which pfUI's `if reagents and reagents[1]` reads correctly (`modules/actionbar.lua:1553`). The subtle part: `spellbook.rs`'s `spell_display` resolves a *known* spell from the book, and a book slot carries no reagent columns — so the book arm now takes reagents from the probe while keeping the book's name/rank/icon. Without that, the spells most likely to be asked about (the ones on your bars) would be exactly the ones wrongly reporting no reagents; a test pins it and was verified to fail otherwise | -| **ClassicAPI tail — the measured remainder** | **6 left of 56** | Re-measured from scratch rather than trusted: of the **153** distinct `C_.` names grom's addons reference, a fresh VM answered all but **56** — 24 where the namespace existed and the member did not, and **32 where the whole namespace was absent**, which is the half that *raises* rather than returning nil. (The earlier sweep's "46 across 15 namespaces" undercounted, and its own grouping had `C_Spell.UnitCastingInfo` down as missing when it was built.) Fifty are now in, in four commits: `C_ChatInfo`, `C_LossOfControl`, `C_EncodingUtil`, `C_EquipmentSet`, `C_LootHistory`, the seven `C_AddOns` members, five on `C_Spell`, two on `C_SpellBook`, three on `C_Item` and the `C_QuestLog` abandon pair. **The six that remain, each with what it needs:** `C_Map.GetAreaTriggerInfo` (pfQuest reads trigger geometry from it instead of a shipped coordinate table — `AreaTrigger.dbc` *is* parsed in `benilla-formats`, so this is a host probe plus the world-point-to-zone resolution `GetMapPositionWorldLoc` already inverts); `C_Item.GetItemSetInfo` (likewise — `ItemSet.dbc` is parsed, and SuperCleveRoids documents the exact table shape it wants, so this is a probe); `C_Item.GetEnchantInfo` and `C_Item.UseAtCursor` (no caller in the corpus beyond the name); `C_FriendList.SetFriendNotes` (1.12 has **no note field on the wire at all** — notes arrived in 2.x — so a note the player typed would live until logout and then be gone, which is worse than a button that does nothing); and `C_BattleNet.SendGameData`, which stays **deliberately absent**: every caller guards it (`_G.C_BattleNet and _G.C_BattleNet.SendGameData or _G.BNSendGameData`), and registering it would claim a BattleNet this client does not have, so the guard is doing exactly its job | +| **ClassicAPI tail — the measured remainder** | **4 left of 56** | Re-measured from scratch rather than trusted: of the **153** distinct `C_.` names grom's addons reference, a fresh VM answered all but **56** — 24 where the namespace existed and the member did not, and **32 where the whole namespace was absent**, which is the half that *raises* rather than returning nil. (The earlier sweep's "46 across 15 namespaces" undercounted, and its own grouping had `C_Spell.UnitCastingInfo` down as missing when it was built.) Fifty are now in, in four commits: `C_ChatInfo`, `C_LossOfControl`, `C_EncodingUtil`, `C_EquipmentSet`, `C_LootHistory`, the seven `C_AddOns` members, five on `C_Spell`, two on `C_SpellBook`, three on `C_Item` and the `C_QuestLog` abandon pair. **Two more went in behind host probes**, each over a DBC 1.12 reads C-side and publishes nowhere: `C_Item.GetItemSetInfo` (the tooltip prints a set block and `GetItemInfo` reports no set at all, so no addon can tell which pieces complete one) and `C_Map.GetAreaTriggerInfo` — pfQuest's own comment says it reads trigger geometry from this "instead of a shipped coordinate table", and the three fields it takes go straight into `pfMap:AddNode`, so the map percentages are the *consumer's* measured shape. The zone resolution is the host's, through the same `zone_uv` law the map window's hover uses, so a trigger lands in the zone the map would name for that point. **The four that remain, each with what it needs:** `C_Item.GetEnchantInfo` and `C_Item.UseAtCursor` (no caller in the corpus beyond the name); `C_FriendList.SetFriendNotes` (1.12 has **no note field on the wire at all** — notes arrived in 2.x — so a note the player typed would live until logout and then be gone, which is worse than a button that does nothing); and `C_BattleNet.SendGameData`, which stays **deliberately absent**: every caller guards it (`_G.C_BattleNet and _G.C_BattleNet.SendGameData or _G.BNSendGameData`), and registering it would claim a BattleNet this client does not have, so the guard is doing exactly its job | | `select` is nil, and real addons call it | open question, maintainer's call | Evidence for the standing stdlib-restore question in `lua_ext.rs`/`string_methods.rs`, not a new one. `lua50.rs` keeps `select` nil for 1.12 fidelity, and pfUI calls `select(7, GetTotemInfo(id))` in live handlers (`modules/totems.lua:18`, `:32`) — so on a real OctoWoW install, where ClassicAPI backports the 5.1 stdlib, those work and here they raise. Currently unreachable (the totem bar is always empty, see that row), so it costs nothing *today*; it is logged because it is the same decision as the string-metatable deviation and should be settled once, for the whole corpus, rather than per call site. Also confirmed while working here: **`#` does not parse in this VM at all**, so any chunk written for it must use `table.getn` | | Lua 5.1 stdlib: `select`, `string.match`, `string.gmatch`, `math.fmod`, `math.modf` | done — **decision taken** | `compat/classic_api/stdlib_backport.rs`. This **resolves** the standing open question above, decided by the maintainer on live evidence (pfUI's `modules/marktracking.lua:300` uses `string.match` to parse a raid-target token and raised 8 times in one session). Scoped by **measurement**, not wholesale: these are the only five members any installed addon calls (12/7/5/2/1 sites). Deliberately still absent — `string.reverse`, `strrev`, `table.maxn`, `math.huge`, the hyperbolics, `coroutine`: nothing calls them, and `table.maxn` would fight `lua50.rs`'s `n`-based table library, which is 5.0 semantics this client needs. **Lives in `compat/`, not `lua50.rs`**: the dialect layer keeps describing 1.12 truthfully and compat re-adds what ClassicAPI adds, so a build that drops `compat/` is still strict 1.12. The restored functions are the **real 5.1 originals**, stashed under `lua50::REG_LUA51_PREFIX` before nil-ing — nothing is reimplemented, because a hand-rolled `string.match` would be a second, subtly different pattern engine, and a test proves the genuine one is in place (position captures, the non-greedy `-` quantifier). **Accepted cost, stated plainly**: an addon writing `if string.gmatch then` now takes the modern branch — which on this server, with ClassicAPI loaded, is the correct answer. Four fidelity gates updated rather than relaxed: `lua50.rs`'s own test now asserts the dialect still lists these on `REMOVED` *and* that they are reachable in the finished VM, so "compat restores it" is not licence for the dialect to stop removing it | | `SetOwner: unknown anchor 'BOTTOMRIGHT'` | **unexplained — diagnostic added** | 566 occurrences in one live session, the loudest line in the log by 20×, and the cause is **not yet known**. Ruled out: every `SetOwner` call site in every installed addon `.lua` and `.xml` passes a valid `ANCHOR_*` string (verified with a paren-matching scan, after a naive regex truncated nested calls and gave a false reading); `script/tooltip/verbs.rs` does no prefix-stripping, matching the full uppercased name; and it does **not** reproduce under the addon harness, so it needs real in-world hovering. It is a *warning*, not an error — the anchor falls back to `ANCHOR_LEFT` and the tooltip still draws, just anchored wrongly. The warning now names the **owner frame**, which distinguishes an addon caller from the stock FrameXML off the player's patch chain; the next session log should identify it. Note `"ANCHOR_TOP"` (`pfUI/modules/mapreveal.lua:44`) and `"NONE"` (`modules/buffwatch.lua:117`) are separately invalid in 1.12 — the reference has nine anchor modes and neither is among them — so defaulting those to Left matches the reference's own silent behaviour |