From 78a983b00e26b008c5999e22f8650d8c28eb7a5a Mon Sep 17 00:00:00 2001 From: Sam Sung <51686767+samwhosung@users.noreply.github.com> Date: Tue, 29 Sep 2026 01:44:52 +0700 Subject: [PATCH] The VM speaks 1.12 alone: the Era globals and the uncalled hooks are gone, the engine's tooltip seams are Rust calls, strata/draw-layer/state-texture/sound/OnClick/quest-item/pickup take their 1.12 shapes, and the surface gate covers the production load (#573) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Lua VM keeps no surface the 1.12.1 client lacks. - **Removed:** the nine Era-only globals: `UnitAura`, `CancelUnitBuff`, `GetCursorInfo`, `GetInventoryItemID`, `GetPlayerFacing`, `GetTradePartnerName`, `IsGossipOptionCoded`, `UnitIsAFK`, `UnitIsDND`. - **Hooks off Lua:** `GameTooltip:BenillaSetItemById` and `:BenillaGetTooltipOwner` are replaced by Rust-side calls. The uncalled `Benilla*` hooks are deleted. - **Frame strata:** `SetFrameStrata` knows the reference's eight strata and raises its message for any other (`0x774450`). - **`SetDrawLayer`** reads the layer name alone (`0x79a780`). - **`SetTexture`** reads a number as a path (`0x781b23`); there is no colour form. - **`PlaySound`** names a kit and returns nothing (`0x4586d0`). `PlaySoundFile` returns one value (`0x458780`); benilla answers 1, a recorded deviation, since it plays after the call. - **`OnClick`** sets only `arg1` (`0x779540`). - **Quest getters:** `GetQuestItemInfo` and the quest-log reward and choice getters answer the reference's five values and its miss behaviour (`0x501f80`, `0x4e0b00`, `0x4e0cf0`). - **`PickupContainerItem`** returns nothing (`0x4f9b30`). - **Surface gates:** the bare VM's globals and every widget class's methods are held to `reference/1.12-globals.tsv` with one commented allowlist. A new gate on the full production load (core plus layer) allows beyond that only the layer's `Benilla`-prefixed globals. Verified: - A test per removal and per corrected form; a scripted undo of each fails its test. - Each gate fails on a re-added name. - `scripts/gates.sh` and `scripts/smoke.sh` are green. - A live run showed the same warnings and 0 script errors before and after. - In the 219-addon corpus, AckisRecipeList's bundled AceGUI-3.0 and Roid-Macros' Grid extension now behave as they do on 1.12.1. Built on macOS; no platform seam touched. Fixes #171 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/benilla-app/src/hover_log.rs | 14 +- crates/benilla-app/src/sound/ui.rs | 1 - crates/benilla-app/src/ui_aura.rs | 5 +- crates/benilla-app/src/ui_aura/units.rs | 32 +- crates/benilla-app/src/ui_gossip/mod.rs | 1 - crates/benilla-app/src/ui_loot/mod.rs | 40 +- crates/benilla-app/src/ui_party/feed.rs | 4 +- .../src/ui_script/action_bar_tests.rs | 11 +- crates/benilla-app/src/ui_script/bag_tests.rs | 7 +- .../src/ui_script/character_tests.rs | 9 +- .../benilla-app/src/ui_script/extract/mod.rs | 8 +- .../benilla-app/src/ui_script/macro_tests.rs | 5 - crates/benilla-app/src/ui_script/mod.rs | 4 + .../benilla-app/src/ui_script/panel_tests.rs | 10 +- .../src/ui_script/pet_bar_tests.rs | 4 +- .../src/ui_script/spellbook_tests.rs | 24 +- .../benilla-app/src/ui_script/surface_gate.rs | 143 +++++++ crates/benilla-app/src/ui_trade/mod.rs | 6 +- crates/benilla-app/tests/trade_frame.rs | 6 +- .../src/spells/dispel_types.rs | 2 +- crates/benilla-ui/src/loader/widgets.rs | 19 +- crates/benilla-ui/src/order.rs | 12 +- crates/benilla-ui/src/script/aura/mod.rs | 342 ++++------------- .../benilla-ui/src/script/aura/player_buff.rs | 18 +- crates/benilla-ui/src/script/bank.rs | 14 +- crates/benilla-ui/src/script/button.rs | 83 +++-- crates/benilla-ui/src/script/char_stats.rs | 43 +-- crates/benilla-ui/src/script/container.rs | 96 ++--- crates/benilla-ui/src/script/cursor.rs | 85 +---- crates/benilla-ui/src/script/cursor/bar.rs | 13 +- crates/benilla-ui/src/script/cursor/doll.rs | 4 +- crates/benilla-ui/src/script/cursor/money.rs | 5 +- crates/benilla-ui/src/script/cursor/pet.rs | 30 +- crates/benilla-ui/src/script/gossip.rs | 23 +- crates/benilla-ui/src/script/loot.rs | 22 -- crates/benilla-ui/src/script/macros.rs | 5 - crates/benilla-ui/src/script/merchant.rs | 122 +----- crates/benilla-ui/src/script/model.rs | 4 +- crates/benilla-ui/src/script/object.rs | 7 +- .../src/script/object/frame_state.rs | 17 +- crates/benilla-ui/src/script/quest.rs | 128 +++++-- crates/benilla-ui/src/script/quest_log.rs | 71 +++- crates/benilla-ui/src/script/region.rs | 16 - crates/benilla-ui/src/script/region/paint.rs | 30 +- crates/benilla-ui/src/script/sound.rs | 109 +++--- crates/benilla-ui/src/script/spellbook.rs | 41 +- crates/benilla-ui/src/script/tests/button.rs | 14 +- .../src/script/tests/reference_surface.rs | 350 +++++++++++++----- crates/benilla-ui/src/script/tests/regions.rs | 35 -- crates/benilla-ui/src/script/tests/tooltip.rs | 31 +- .../src/script/tests/tooltip_item/law.rs | 38 +- .../script/tests/tooltip_item/mechanics.rs | 12 +- .../src/script/tests/tooltip_item/mod.rs | 10 +- .../src/script/tests/tooltip_item/set.rs | 8 +- .../src/script/tests/widget_surface.rs | 5 +- .../benilla-ui/src/script/tests/worldframe.rs | 5 +- .../benilla-ui/src/script/tests/worldmap.rs | 3 +- crates/benilla-ui/src/script/tooltip/mod.rs | 15 + crates/benilla-ui/src/script/tooltip/verbs.rs | 20 +- .../benilla-ui/src/script/tooltip_item/mod.rs | 19 +- crates/benilla-ui/src/script/tooltip_spell.rs | 11 +- crates/benilla-ui/src/script/trade.rs | 32 +- crates/benilla-ui/src/script/types.rs | 6 +- crates/benilla-ui/src/script/unit/bindings.rs | 15 +- crates/benilla-ui/src/script/unit/mod.rs | 4 - crates/benilla-ui/src/script/unit/tests.rs | 30 -- crates/benilla-ui/src/script/worldmap.rs | 9 - crates/benilla-ui/src/widget/mod.rs | 3 +- reference/README.md | 8 +- 69 files changed, 1103 insertions(+), 1275 deletions(-) create mode 100644 crates/benilla-app/src/ui_script/surface_gate.rs diff --git a/crates/benilla-app/src/hover_log.rs b/crates/benilla-app/src/hover_log.rs index 66133912..99725980 100644 --- a/crates/benilla-app/src/hover_log.rs +++ b/crates/benilla-app/src/hover_log.rs @@ -99,25 +99,25 @@ impl Plugin for HoverLogPlugin { } } -/// The tooltip context, asked of the Lua VM, once per frame while recording. +/// The tooltip context, asked of the Lua VM, once per frame while recording; the owner, which 1.12 +/// does not answer to Lua, from the engine. fn tooltip_context(script: &UiScript) -> Option<(String, i64, String)> { let chunk = r#" if not GameTooltip or not GameTooltip:IsShown() then return "" end - local owner = GameTooltip:BenillaGetTooltipOwner() - local name = "(no owner)" - if owner and owner.GetName and owner:GetName() then name = owner:GetName() end - local first = "" + local first = " if GameTooltipTextLeft1 and GameTooltipTextLeft1:GetText() then first = GameTooltipTextLeft1:GetText() end - return name .. "\t" .. GameTooltip:NumLines() .. "\t" .. first + return GameTooltip:NumLines() .. "\t" .. first "#; let s: String = script.eval(chunk).ok()?; if s.is_empty() { return None; } let mut parts = s.split('\t'); - let owner = parts.next()?.to_string(); + let owner = script + .tooltip_owner_name("GameTooltip") + .unwrap_or_else(|| "(no owner)".to_string()); let lines = parts.next()?.trim().parse::().ok()? as i64; let first = parts.next().unwrap_or("").to_string(); Some((owner, lines, first)) diff --git a/crates/benilla-app/src/sound/ui.rs b/crates/benilla-app/src/sound/ui.rs index 304ba119..a135f3b0 100644 --- a/crates/benilla-app/src/sound/ui.rs +++ b/crates/benilla-app/src/sound/ui.rs @@ -48,7 +48,6 @@ fn drain_ui_sounds( }; for req in requests { let kit_ref = match &req { - SoundRequest::KitId(id) => KitRef::Id(*id), SoundRequest::KitName(name) => KitRef::Name(name), SoundRequest::File(path) => { // `PlaySoundFile`: by path, with no kit gates or variation. diff --git a/crates/benilla-app/src/ui_aura.rs b/crates/benilla-app/src/ui_aura.rs index 071d1fd9..199c7b2c 100644 --- a/crates/benilla-app/src/ui_aura.rs +++ b/crates/benilla-app/src/ui_aura.rs @@ -84,7 +84,7 @@ pub(crate) struct PlayerAuraCache { #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] pub(crate) struct AuraEvents; -/// Adds the aura feed and the `CancelUnitBuff` drain. +/// Adds the aura feed and the `CancelPlayerBuff`/`CancelTrackingBuff` drain. pub(crate) struct UiAuraPlugin; impl Plugin for UiAuraPlugin { @@ -629,7 +629,8 @@ fn end_session_aura_state( // No `AuraFeedMemory` reset: its edge keys die with the VM. } -/// A `CMSG_CANCEL_AURA` per queued `CancelUnitBuff`, naming the spell, not the slot (`0x6e7040`). +/// A `CMSG_CANCEL_AURA` per queued `CancelPlayerBuff` or `CancelTrackingBuff`, naming the spell, +/// not the slot (`0x6e7040`). fn drain_aura_cancels(script: Option>, net: Res) { let Some(mut script) = script else { return; diff --git a/crates/benilla-app/src/ui_aura/units.rs b/crates/benilla-app/src/ui_aura/units.rs index 40eeb002..06bed75f 100644 --- a/crates/benilla-app/src/ui_aura/units.rs +++ b/crates/benilla-app/src/ui_aura/units.rs @@ -419,27 +419,29 @@ mod tests { .apply_stats(guid, full, info); } - /// The spell ids `UnitBuff` (or `UnitDebuff`) enumerates for `token`, index 1 up to the nil, - /// through `UnitAura`'s tenth return, as the icon needs a spell catalog. + /// The spell ids of `token`'s pushed debuffs (`harmful`) or buffs, after checking that + /// `UnitDebuff` (or `UnitBuff`) enumerates exactly that many, index 1 up to the nil. fn listed(app: &mut App, token: &str, harmful: bool) -> Vec { - let (verb, filter) = if harmful { - ("UnitDebuff", "HARMFUL") - } else { - ("UnitBuff", "HELPFUL") - }; - app.world_mut() - .non_send_resource::() - .eval::>(&format!( - r#"local out = {{}} + let verb = if harmful { "UnitDebuff" } else { "UnitBuff" }; + let s = app.world_mut().non_send_resource::(); + let ids = s.aura_spell_ids(token, !harmful); + let enumerated = s + .eval::(&format!( + r#"local n = 0 for i = 1, 48 do local _, count = {verb}("{token}", i) if count == nil then break end - local _, _, _, _, _, _, _, _, _, id = UnitAura("{token}", i, "{filter}") - table.insert(out, id) + n = n + 1 end - return out"# + return n"# )) - .unwrap() + .unwrap(); + assert_eq!( + enumerated, + ids.len(), + "{verb}(\"{token}\", i) enumerates the list" + ); + ids.into_iter().map(i64::from).collect() } fn seen(app: &mut App) -> Vec { diff --git a/crates/benilla-app/src/ui_gossip/mod.rs b/crates/benilla-app/src/ui_gossip/mod.rs index 02bb88a2..9186fb0e 100644 --- a/crates/benilla-app/src/ui_gossip/mod.rs +++ b/crates/benilla-app/src/ui_gossip/mod.rs @@ -214,7 +214,6 @@ fn snapshot(state: &GossipState) -> Option { .map(|o| GossipOptionView { label: o.message.clone(), icon_type: gossip_icon_type(o.icon).into(), - coded: o.coded, }) .collect(), }) diff --git a/crates/benilla-app/src/ui_loot/mod.rs b/crates/benilla-app/src/ui_loot/mod.rs index 443e66e0..c91b67e4 100644 --- a/crates/benilla-app/src/ui_loot/mod.rs +++ b/crates/benilla-app/src/ui_loot/mod.rs @@ -839,7 +839,7 @@ fn close_on_move_start( } /// Sends what the Lua asked for, as the take dispatcher `0x4c2790(slot, flag)`: a row click -/// (`BenillaTakeLootSlot`, flag 0) and the `LOOT_BIND` confirm (`LootSlot`, flag 1) stay apart, +/// (a `LootButton`'s own, flag 0) and the `LOOT_BIND` confirm (`LootSlot`, flag 1) stay apart, /// so a second click on a bind-on-pickup row asks again. fn drain_loot( script: Option>, @@ -896,7 +896,7 @@ fn drain_loot( // At the click, before any answer (`0x4c2926`): the item's ItemGroupSounds kit 0. pickup.write(crate::sound::LootPickupSound { display_id }); } - None => debug!("ui_loot: BenillaTakeLootSlot({index}) out of range — ignored"), + None => debug!("ui_loot: row {index} clicked out of range — ignored"), } } @@ -1069,9 +1069,12 @@ mod tests { const SECOND_BOP: u32 = 18832; /// `rows` open on a corpse with every template landed, then `lua` and one drain. + /// A loot window over `rows` whose display rows `clicks` are clicked by the mouse, as a + /// `LootButton` takes (`0x4c1820`), then `lua` run, then one drain. fn drain_with( gold: u32, rows: Vec, + clicks: &[u32], lua: &str, ) -> (App, crossbeam_channel::Receiver) { let (tx, rx) = crossbeam_channel::unbounded(); @@ -1106,7 +1109,7 @@ mod tests { .resource_mut::() .open(0x42, loot_type::CORPSE, gold, rows); - let script = UiScript::new().unwrap(); + let mut script = UiScript::new().unwrap(); script .run( "BIND_CONFIRMS = {}\n\ @@ -1115,6 +1118,19 @@ mod tests { f:SetScript(\"OnEvent\", function() tinsert(BIND_CONFIRMS, arg1) end)", ) .unwrap(); + script.set_screen_size(1024.0, 768.0); + for &row in clicks { + script + .run(&format!( + "local b = CreateFrame(\"LootButton\", \"DrainRow\", UIParent)\n\ + b:SetPoint(\"BOTTOMLEFT\", 100, 100) b:SetWidth(50) b:SetHeight(50)\n\ + b:EnableMouse(true) b:Show() b:SetSlot({row})" + )) + .unwrap(); + script.resolve(); + script.mouse_button(125.0, 125.0, "LeftButton", true); + script.mouse_button(125.0, 125.0, "LeftButton", false); + } script.run(lua).unwrap(); app.insert_non_send_resource(script); app.world_mut().run_system_once(drain_loot).unwrap(); @@ -1220,7 +1236,7 @@ mod tests { /// The click arm's deferral (`0x4c28f2`-`0x4c2920`). #[test] fn a_bop_row_confirms_instead_of_sending() { - let (mut app, rx) = drain_with(0, vec![item(0, FELSTRIKER, 1)], "BenillaTakeLootSlot(1)"); + let (mut app, rx) = drain_with(0, vec![item(0, FELSTRIKER, 1)], &[1], ""); assert!(sent(&rx).is_empty(), "the deferred take sends nothing"); assert_eq!(pending_confirm(&app), Some(1), "and stashes the row"); @@ -1234,11 +1250,7 @@ mod tests { /// The confirm arm (`0x4c27c0`). #[test] fn loot_slot_completes_the_pending_confirm_exactly_once() { - let (mut app, rx) = drain_with( - 0, - vec![item(7, FELSTRIKER, 1)], - "BenillaTakeLootSlot(1) LootSlot(1)", - ); + let (mut app, rx) = drain_with(0, vec![item(7, FELSTRIKER, 1)], &[1], "LootSlot(1)"); assert!( matches!( sent(&rx)[..], @@ -1263,6 +1275,7 @@ mod tests { let (_app, rx) = drain_with( 0, vec![item(0, TOUGH_JERKY, 1), item(1, FLURRY_AXE, 1)], + &[], "LootSlot(1) LootSlot(2)", ); assert!( @@ -1278,7 +1291,7 @@ mod tests { (WHITE_BOP, "quality 1 is below the floor of 2"), (TOUGH_JERKY, "neither"), ] { - let (app, rx) = drain_with(0, vec![item(3, entry, 1)], "BenillaTakeLootSlot(1)"); + let (app, rx) = drain_with(0, vec![item(3, entry, 1)], &[1], ""); assert!( matches!( sent(&rx)[..], @@ -1300,7 +1313,8 @@ mod tests { let (mut app, rx) = drain_with( 120, // gold, so display row 1 is the coin and the item is row 2 vec![item(4, FELSTRIKER, 1)], - "BenillaTakeLootSlot(2)", + &[2], + "", ); assert_eq!( pending_confirm(&app), @@ -1325,7 +1339,7 @@ mod tests { /// `0x4c27d7`: the continuation returns on an emptied record. #[test] fn an_accept_for_a_row_that_was_taken_away_sends_nothing() { - let (mut app, rx) = drain_with(0, vec![item(2, FELSTRIKER, 1)], "BenillaTakeLootSlot(1)"); + let (mut app, rx) = drain_with(0, vec![item(2, FELSTRIKER, 1)], &[1], ""); assert_eq!(pending_confirm(&app), Some(1)); app.world_mut().resource_mut::().remove_slot(2); @@ -1344,7 +1358,7 @@ mod tests { /// `0x4c1df5`: the response copier resets the stash. #[test] fn a_pending_confirm_does_not_survive_the_window() { - let (mut app, _rx) = drain_with(0, vec![item(0, FELSTRIKER, 1)], "BenillaTakeLootSlot(1)"); + let (mut app, _rx) = drain_with(0, vec![item(0, FELSTRIKER, 1)], &[1], ""); assert_eq!(pending_confirm(&app), Some(1)); app.world_mut().resource_mut::().clear(); diff --git a/crates/benilla-app/src/ui_party/feed.rs b/crates/benilla-app/src/ui_party/feed.rs index 7e0d87be..f2cc6489 100644 --- a/crates/benilla-app/src/ui_party/feed.rs +++ b/crates/benilla-app/src/ui_party/feed.rs @@ -663,7 +663,7 @@ fn member_unit_state( // `+0x08 & 4`) and `UnitIsGhost` (`0x517c32`, `& 8`) read it: fresher than the roster // byte, which only `SMSG_GROUP_LIST` moves. Connected stays the roster's, though the // reference reads the record's bit 0 (`0x517dd3`); its no-object PvP and FFA reads are - // untraced, and `UnitIsAFK`/`UnitIsDND` are not 1.12 bindings. + // untraced, and 1.12 has no AFK or DND predicate to feed. dead: stats.is_some_and(|s| s.status.unwrap_or(0) & member_status::DEAD != 0), ghost: stats.is_some_and(|s| s.status.unwrap_or(0) & member_status::GHOST != 0), ..Default::default() @@ -677,8 +677,6 @@ fn member_unit_state( s.faction_group = own_group; // The roster status byte overlays both legs. s.is_connected = m.status & member_status::ONLINE != 0; - s.is_afk = m.status & member_status::AFK != 0; - s.is_dnd = m.status & member_status::DND != 0; s.is_pvp_ffa = m.status & member_status::PVP_FFA != 0; s.pvp = s.pvp || m.status & member_status::PVP != 0; s.ghost = s.ghost || m.status & member_status::GHOST != 0; diff --git a/crates/benilla-app/src/ui_script/action_bar_tests.rs b/crates/benilla-app/src/ui_script/action_bar_tests.rs index a73dd0b3..ef05f764 100644 --- a/crates/benilla-app/src/ui_script/action_bar_tests.rs +++ b/crates/benilla-app/src/ui_script/action_bar_tests.rs @@ -840,13 +840,10 @@ fn drag_drop_onto_another_button_hops_the_displaced_action() { "Interface\\Icons\\Spell_A", "slot 2 now shows the placed action" ); - let (kind, src) = s - .eval::<(String, i64)>("local k, slot = GetCursorInfo() return k, slot") - .unwrap(); - assert_eq!( - (kind.as_str(), src), - ("action", 2), - "the displaced action hopped on, sourced from slot 2" + assert!( + matches!(s.cursor_payload(), Some(benilla_ui::script::CursorPayload::Action(c)) if c.src_slot == 2), + "the displaced action hopped on, sourced from slot 2: {:?}", + s.cursor_payload() ); // Two independent sends across the one gesture: the pickup's clear, then the place's write. diff --git a/crates/benilla-app/src/ui_script/bag_tests.rs b/crates/benilla-app/src/ui_script/bag_tests.rs index ce077ad9..6ee902bb 100644 --- a/crates/benilla-app/src/ui_script/bag_tests.rs +++ b/crates/benilla-app/src/ui_script/bag_tests.rs @@ -1630,8 +1630,11 @@ fn the_keyring_button_opens_a_keyring_window() { .unwrap(); assert_eq!(shown, 8, "only the level-unlocked keyring slots are drawn"); assert_eq!( - s.eval::("return BenillaGetContainerItemID(KEYRING_CONTAINER, 1)") - .unwrap(), + s.eval::( + "local _, _, id = string.find(GetContainerItemLink(KEYRING_CONTAINER, 1), 'item:(%d+)') \ + return tonumber(id)" + ) + .unwrap(), 7146 ); diff --git a/crates/benilla-app/src/ui_script/character_tests.rs b/crates/benilla-app/src/ui_script/character_tests.rs index 2c67f162..a4189c76 100644 --- a/crates/benilla-app/src/ui_script/character_tests.rs +++ b/crates/benilla-app/src/ui_script/character_tests.rs @@ -442,10 +442,11 @@ fn clicking_an_occupied_doll_slot_picks_it_up_and_locks_it() { assert!(s.errors().is_empty(), "click errors: {:?}", s.errors()); assert!(s.eval::("return CursorHasItem()").unwrap()); - let (kind, id) = s - .eval::<(String, i64)>("local k, id = GetCursorInfo() return k, id") - .unwrap(); - assert_eq!((kind.as_str(), id), ("item", 1234)); + assert!( + matches!(s.cursor_payload(), Some(benilla_ui::script::CursorPayload::Item(c)) if c.item_id == 1234), + "the cursor holds item 1234: {:?}", + s.cursor_payload() + ); assert!( s.eval::("return IsInventoryItemLocked(1)").unwrap(), "the picked slot locks" diff --git a/crates/benilla-app/src/ui_script/extract/mod.rs b/crates/benilla-app/src/ui_script/extract/mod.rs index 8afc849a..8146047c 100644 --- a/crates/benilla-app/src/ui_script/extract/mod.rs +++ b/crates/benilla-app/src/ui_script/extract/mod.rs @@ -1278,7 +1278,7 @@ fn convert_entry( }, color: [1.0, 1.0, 1.0, eq.alpha], // `SetPortraitTexture` cuts the inscribed circle, as the reference stamps into - // its 64² bake's alpha; `BenillaSetBoothTexture` samples square. + // its 64² bake's alpha. circular, premultiplied, clip, @@ -2094,15 +2094,15 @@ mod extract_gate_tests { let mut app = app_with_marker(); app.world_mut() .non_send_resource_mut::() - .run("BenillaSetBoothTexture(marker, 'paperdoll')") + .run("SetPortraitTexture(marker, 'target')") .unwrap(); - // The booth publishes a live bake for that slot; without an entry the region draws nothing. + // The booth publishes a live bake for that unit; without an entry the region draws nothing. let bake = app .world_mut() .resource_mut::>() .add(Image::default()); app.world_mut().resource_mut::().0.insert( - "paperdoll".to_string(), + "target".to_string(), crate::portrait::PortraitSource::Live(bake.clone()), ); app.update(); diff --git a/crates/benilla-app/src/ui_script/macro_tests.rs b/crates/benilla-app/src/ui_script/macro_tests.rs index 1b6d15c2..61a2b078 100644 --- a/crates/benilla-app/src/ui_script/macro_tests.rs +++ b/crates/benilla-app/src/ui_script/macro_tests.rs @@ -292,11 +292,6 @@ fn dragging_a_macro_button_loads_the_cursor_with_the_macro_payload() { matches!(&payload, Some(CursorPayload::Macro(m)) if m.index == 1), "the macro payload, carrying its index: {payload:?}" ); - assert_eq!( - s.eval::<(String, i64)>("local k, i = GetCursorInfo() return k, i") - .unwrap(), - ("macro".to_string(), 1) - ); // A bar slot packs the MACRO tag (0x40 << 24) with the macro index. let mut s = s; diff --git a/crates/benilla-app/src/ui_script/mod.rs b/crates/benilla-app/src/ui_script/mod.rs index 7d9f5909..f61b6af0 100644 --- a/crates/benilla-app/src/ui_script/mod.rs +++ b/crates/benilla-app/src/ui_script/mod.rs @@ -753,6 +753,10 @@ mod panel_template_tests; #[cfg(test)] mod shape_gate; +/// The surface gate: the production load's globals and widget methods against the 1.12 tables. +#[cfg(test)] +mod surface_gate; + /// The event argument-shape gate: every fire site against `reference/1.12-events.tsv`. #[cfg(test)] mod event_shape_gate; diff --git a/crates/benilla-app/src/ui_script/panel_tests.rs b/crates/benilla-app/src/ui_script/panel_tests.rs index 2e84d741..c7a08ca0 100644 --- a/crates/benilla-app/src/ui_script/panel_tests.rs +++ b/crates/benilla-app/src/ui_script/panel_tests.rs @@ -68,12 +68,10 @@ fn shipped_gossip_frame_drives_end_to_end() { GossipOptionView { label: "Let me browse your goods.".into(), icon_type: "vendor".into(), - coded: false, }, GossipOptionView { label: "I wish to sign the petition.".into(), icon_type: "gossip".into(), - coded: true, }, ], })); @@ -86,8 +84,7 @@ fn shipped_gossip_frame_drives_end_to_end() { .unwrap(), "Greetings, traveler. How may I help you?" ); - // The coded row is enabled too: `GossipFrameOptionsUpdate` has no coded-option handling - // (`GossipFrame.lua:111-128`). A coded option is sent without a code; that path is not built. + // Every row is enabled: `GossipFrameOptionsUpdate` greys none (`GossipFrame.lua:111-128`). let states: (bool, bool, bool, bool) = s .eval( "return GossipTitleButton1:IsVisible(), GossipTitleButton1:IsEnabled() ~= 0,\n\ @@ -229,7 +226,6 @@ fn shipped_gossip_frame_renders_quest_rows_above_options() { options: vec![GossipOptionView { label: "Let me browse your goods.".into(), icon_type: "vendor".into(), - coded: false, }], })); s.fire_event("GOSSIP_SHOW", vec![]); @@ -338,7 +334,6 @@ fn shipped_gossip_rows_grow_to_their_wrapped_labels() { let long = |t: &str| GossipOptionView { label: t.into(), icon_type: "gossip".into(), - coded: false, }; s.set_gossip(Some(GossipMenu { greeting: "Make your choice!".into(), @@ -490,7 +485,6 @@ fn shipped_panel_slot_replaces_gossip_with_merchant() { options: vec![GossipOptionView { label: "Let me browse your goods.".into(), icon_type: "vendor".into(), - coded: false, }], })); s.fire_event("GOSSIP_SHOW", vec![]); @@ -724,7 +718,6 @@ fn gossip_bank_option_hands_the_left_slot_to_the_bank() { options: vec![GossipOptionView { label: "I would like to check my deposit box.".into(), icon_type: "money".into(), - coded: false, }], })); s.fire_event("GOSSIP_SHOW", vec![]); @@ -793,7 +786,6 @@ fn an_overflowing_gossip_menu_scrolls_instead_of_spilling() { broken the law of the land and it is my sworn duty to enforce it, whatever the cost." ), icon_type: "gossip".into(), - coded: false, }; s.set_gossip(Some(GossipMenu { greeting: "Make your choice!".into(), diff --git a/crates/benilla-app/src/ui_script/pet_bar_tests.rs b/crates/benilla-app/src/ui_script/pet_bar_tests.rs index bb30c54b..59ca9f8c 100644 --- a/crates/benilla-app/src/ui_script/pet_bar_tests.rs +++ b/crates/benilla-app/src/ui_script/pet_bar_tests.rs @@ -443,7 +443,7 @@ fn dragging_a_pet_spell_between_slots_moves_it_through_the_shipped_handlers() { 0, "the grid goes down with the payload" ); - assert!(s.eval::("return GetCursorInfo() == nil").unwrap()); + assert!(s.cursor_payload().is_none()); } /// `LOCK_ACTIONBAR` stops both drag ends (PetActionBarFrame.lua:270,278) but not the shift-click @@ -466,7 +466,7 @@ fn the_lock_stops_the_pet_bar_drag_but_not_its_shift_click() { s.take_pet_set_actions().is_empty(), "a locked bar sends nothing — the slot was never picked up" ); - assert!(s.eval::("return GetCursorInfo() == nil").unwrap()); + assert!(s.cursor_payload().is_none()); s.set_modifiers(true, false, false); s.run("this = PetActionButton4; PetActionButton_OnClick(\"LeftButton\")") diff --git a/crates/benilla-app/src/ui_script/spellbook_tests.rs b/crates/benilla-app/src/ui_script/spellbook_tests.rs index 84bb9d92..66ed8a6d 100644 --- a/crates/benilla-app/src/ui_script/spellbook_tests.rs +++ b/crates/benilla-app/src/ui_script/spellbook_tests.rs @@ -203,14 +203,13 @@ fn shipped_spellbook_drives_end_to_end() { s.mouse_button(x1, y1, "LeftButton", false); s.set_modifiers(false, false, false); assert!(s.take_spell_casts().is_empty(), "shift-click never casts"); - let (kind, _book_id, book_type, spell_id) = s - .eval::<(String, i64, String, i64)>( - "local k, slot, book, id = GetCursorInfo() return k, slot, book, id", - ) - .unwrap(); - assert_eq!( - (kind.as_str(), book_type.as_str(), spell_id), - ("spell", "spell", 133) + assert!( + matches!( + s.cursor_payload(), + Some(benilla_ui::script::CursorPayload::Spell(c)) if c.book_type == "spell" && c.spell_id == 133 + ), + "the cursor holds spell 133 from the spell book: {:?}", + s.cursor_payload() ); // An empty slot is hidden (`ActionButton.lua:214-215`) until a held payload opens the grid; @@ -784,9 +783,10 @@ fn the_macro_editor_takes_a_shift_click_and_only_a_shift_click() { click(&mut s, "SpellButton1", "LeftButton"); s.set_modifiers(false, false, false); assert!(s.errors().is_empty(), "pickup errors: {:?}", s.errors()); - let (kind, spell_id) = s - .eval::<(String, i64)>("local k, _, _, id = GetCursorInfo() return k, id") - .unwrap(); - assert_eq!((kind.as_str(), spell_id), ("spell", 133)); + assert!( + matches!(s.cursor_payload(), Some(benilla_ui::script::CursorPayload::Spell(c)) if c.spell_id == 133), + "the cursor holds spell 133: {:?}", + s.cursor_payload() + ); assert!(s.take_spell_casts().is_empty()); } diff --git a/crates/benilla-app/src/ui_script/surface_gate.rs b/crates/benilla-app/src/ui_script/surface_gate.rs new file mode 100644 index 00000000..497a444e --- /dev/null +++ b/crates/benilla-app/src/ui_script/surface_gate.rs @@ -0,0 +1,143 @@ +//! The 1.12 surface of the production load as a gate: every global and widget method an addon +//! reaches once the core and benilla's layer have loaded is one the 1.12.1 client has +//! (`reference/1.12-globals.tsv`), one the engine exposes, or a `Benilla`-prefixed name the layer +//! defines. The engine's own surface is gated on a bare VM in `benilla-ui`, name by name against +//! the same table (`script::tests::reference_surface`, whose commented `BEYOND_1_12` is the one +//! allowlist) and method by method against each class's registrar tables +//! (`script::tests::widget_surface`); so here the load itself may add nothing else. + +use std::collections::{BTreeSet, HashSet}; + +use benilla_ui::script::{widget_method_census, UiScript}; + +use crate::local_state::test_env::ENV_LOCK; + +/// `reference/1.12-globals.tsv`'s names. +fn reference_globals() -> HashSet<&'static str> { + let tsv = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../reference/1.12-globals.tsv" + )); + let names: HashSet<&str> = tsv + .lines() + .filter(|l| !l.starts_with('#')) + .filter_map(|l| l.split('\t').next()) + .collect(); + assert!( + names.len() > 19_000, + "the reference table looks truncated ({} names)", + names.len() + ); + names +} + +/// The string keys of `_G`. +fn globals_of(s: &UiScript) -> BTreeSet { + s.eval::>( + "local out = {} for k in pairs(getfenv(0)) do \ + if type(k) == 'string' then table.insert(out, k) end end return out", + ) + .expect("dump the globals") + .into_iter() + .collect() +} + +/// Every `(class, method)` a VM answers, as `Class:Method`. The WorldFrame type is one-shot +/// (`0x6ee439`), spent once the stock `WorldFrame` exists, so a loaded VM cannot make a second; +/// its methods are a Frame's, which the census asks of a Frame. +fn methods_of(s: &UiScript) -> BTreeSet { + widget_method_census(s) + .expect("the widget census") + .into_iter() + .filter_map(|(class, method)| { + if method == "!NOT-INSTANTIABLE" { + assert_eq!(class, "WorldFrame", "{class} could not be made"); + return None; + } + Some(format!("{class}:{method}")) + }) + .collect() +} + +/// A name the layer may define: its prefix, or a slash alias's `SLASH_BENILLA_` form. +fn layer_prefixed(n: &str) -> bool { + n.starts_with("Benilla") || n.starts_with("BENILLA_") || n.starts_with("SLASH_BENILLA_") +} + +/// What the production load adds past 1.12 and past the engine, and the layer's own names: the +/// globals (`Benilla`-prefixed ones the engine does not also define passing) and the methods. +fn production_beyond(s: &UiScript, engine: &UiScript) -> (Vec, Vec) { + let reference = reference_globals(); + let engine_globals = globals_of(engine); + let globals = globals_of(s) + .into_iter() + .filter(|n| !reference.contains(n.as_str()) && !engine_globals.contains(n)) + .filter(|n| !layer_prefixed(n)) + .collect(); + let engine_methods = methods_of(engine); + let methods = methods_of(s) + .into_iter() + .filter(|m| !engine_methods.contains(m)) + .collect(); + (globals, methods) +} + +/// The production load, core and layer, adds no global past 1.12 but the layer's own +/// `Benilla`-prefixed names, and no widget method at all. +#[test] +fn the_production_load_stays_inside_the_1_12_surface() { + benilla_formats::wow_data_or_skip!(); + let _l = ENV_LOCK + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let (s, failures) = super::layer_tests::production_load_with("surface", false, "", |_| {}); + assert!(failures.is_empty(), "load failures: {failures:#?}"); + let engine = UiScript::new().unwrap(); + // The layer's globals are there to be checked: a load that defined none proves nothing. + assert!( + globals_of(&s) + .iter() + .any(|n| layer_prefixed(n) && !globals_of(&engine).contains(n)), + "the layer defined no global: it did not load" + ); + + let (globals, methods) = production_beyond(&s, &engine); + assert!( + globals.is_empty(), + "the production load exposes {} global(s) the 1.12.1 client does not:\n {}\n\n\ + A layer global takes the `Benilla` prefix; anything else gets its 1.12 spelling or goes.", + globals.len(), + globals.join(" ") + ); + assert!( + methods.is_empty(), + "the production load adds {} widget method(s) the engine does not register:\n {}", + methods.len(), + methods.join(" ") + ); +} + +/// The gate bites: a global past 1.12 put back into the loaded VM, an unprefixed layer-style one, +/// an engine-only prefixed one and a method on a class's table are each caught. +#[test] +fn the_production_gate_catches_a_name_put_back() { + let s = UiScript::new().unwrap(); + let engine = UiScript::new().unwrap(); + assert_eq!( + production_beyond(&s, &engine), + (Vec::new(), Vec::new()), + "two bare VMs differ" + ); + s.run( + "UnitAura = function() end \ + BenillaLayerThing = 1 \ + getmetatable(CreateFrame('GameTooltip')).__index.BenillaSetItemById = function() end", + ) + .unwrap(); + let (globals, methods) = production_beyond(&s, &engine); + assert_eq!(globals, vec!["UnitAura".to_string()]); + assert!( + methods.contains(&"GameTooltip:BenillaSetItemById".to_string()), + "{methods:?}" + ); +} diff --git a/crates/benilla-app/src/ui_trade/mod.rs b/crates/benilla-app/src/ui_trade/mod.rs index fc67658c..5b6df981 100644 --- a/crates/benilla-app/src/ui_trade/mod.rs +++ b/crates/benilla-app/src/ui_trade/mod.rs @@ -525,7 +525,6 @@ fn snapshot( trade: &TradeSession, items: &Items, icons: Option<&ItemDisplays>, - names: &NameCache, commands: &NetCommands, ) -> Option { if !trade.open { @@ -534,9 +533,6 @@ fn snapshot( Some(TradeState { player: resolve_side(&trade.our, items, icons, commands), target: resolve_side(&trade.their, items, icons, commands), - partner_name: trade - .partner - .and_then(|g| names.resolve(g, commands).map(str::to_string)), }) } @@ -568,7 +564,7 @@ fn feed_trade( let last_player_gold = last_player_gold.get(&script); let last_their_gold = last_their_gold.get(&script); - let fresh = snapshot(&trade, &items, icons.as_deref(), &names, &commands); + let fresh = snapshot(&trade, &items, icons.as_deref(), &commands); let opened = !*last_open && trade.is_open(); let closed = *last_open && !trade.is_open(); let changed = fresh != *last; diff --git a/crates/benilla-app/tests/trade_frame.rs b/crates/benilla-app/tests/trade_frame.rs index 026c8f2c..2b7a3492 100644 --- a/crates/benilla-app/tests/trade_frame.rs +++ b/crates/benilla-app/tests/trade_frame.rs @@ -57,11 +57,7 @@ fn state() -> TradeState { ..Default::default() }; target.slots[0] = Some(item(4306, "Silk Cloth", 1, 2)); - TradeState { - player, - target, - partner_name: Some("Thrall".into()), - } + TradeState { player, target } } #[test] diff --git a/crates/benilla-formats/src/spells/dispel_types.rs b/crates/benilla-formats/src/spells/dispel_types.rs index 6338cd1e..9818fc79 100644 --- a/crates/benilla-formats/src/spells/dispel_types.rs +++ b/crates/benilla-formats/src/spells/dispel_types.rs @@ -1,5 +1,5 @@ //! `SpellDispelType.dbc`: the dispel class name the aura tooltip (`0x52f880`) shows and -//! `UnitAura`/`UnitDebuff` return as `debuffType`, which FrameXML's `DebuffTypeColor` keys the +//! `UnitDebuff` returns as its `dispelType`, which FrameXML's `DebuffTypeColor` keys the //! debuff border on. A row is named only when column 10 (`+0x28`) is nonzero (`0x52f906`): on 5875 //! that is ids 1-4, so Stealth and Invisibility carry names that never print. Column 11 repeats //! the name on those four rows; the tooltip does not read it. diff --git a/crates/benilla-ui/src/loader/widgets.rs b/crates/benilla-ui/src/loader/widgets.rs index 34c39519..a0c5e5fd 100644 --- a/crates/benilla-ui/src/loader/widgets.rs +++ b/crates/benilla-ui/src/loader/widgets.rs @@ -83,18 +83,19 @@ impl Loader<'_> { // form (`UIPanelButtonUpTexture`); it expands like a `` region. let expanded = this.expand_region(raw); let t = &expanded; - if let Some(file) = t.attr("file") { - this.call(wrapper, method, file.to_string(), dbg); - } else if let Some(c) = children_named(t, "Color").next().map(color_of) { - this.call(wrapper, method, (c[0], c[1], c[2], c[3]), dbg); - } else { - // No art: the element alone creates the region, as the reference sends all four - // state textures through the `` texture adder (`0x6f26f0`, from - // `0x778903`); `""` creates the slot without painting it. - this.call(wrapper, method, String::new(), dbg); + // The reference sends all four state textures through the `` texture adder + // (`0x6f26f0`, from `0x778903`), so a `` is the region's own colour fill; the + // Lua setter has no colour form (`0x781970`). `""` creates the slot unpainted. + let color = children_named(t, "Color").next().map(color_of); + match t.attr("file") { + Some(file) => this.call(wrapper, method, file.to_string(), dbg), + None => this.call(wrapper, method, String::new(), dbg), } let getter = method.replacen("Set", "Get", 1); if let Ok(region) = wrapper.call_method::(getter.as_str(), ()) { + if let (None, Some(c)) = (t.attr("file"), color) { + this.call_region(®ion, "SetTexture", (c[0], c[1], c[2], c[3]), dbg); + } if let Some(mode) = t.attr("alphaMode") { this.call_region(®ion, "SetBlendMode", mode.to_string(), dbg); } diff --git a/crates/benilla-ui/src/order.rs b/crates/benilla-ui/src/order.rs index db089796..ac29e455 100644 --- a/crates/benilla-ui/src/order.rs +++ b/crates/benilla-ui/src/order.rs @@ -37,13 +37,11 @@ pub enum Strata { Fullscreen = 6, FullscreenDialog = 7, Tooltip = 8, - /// A later client's stratum above `Tooltip`, not a 1.12 one. - Blizzard = 9, } impl Strata { /// Every stratum in draw order. - pub const ALL: [Strata; 10] = [ + pub const ALL: [Strata; 9] = [ Strata::World, Strata::Background, Strata::Low, @@ -53,7 +51,6 @@ impl Strata { Strata::Fullscreen, Strata::FullscreenDialog, Strata::Tooltip, - Strata::Blizzard, ]; /// The client bucket id. @@ -114,8 +111,9 @@ impl Default for DrawLayer { // plus 1 (`SetParent` `0x76ab10`, at `0x76ab65`), `MirrorTimer.xml:62` lowers the bar by 1 to tie // its parent, and the tie lets the bar's ARTWORK fill draw under the parent's OVERLAY border. // -// The sub-level is a later client's ordering within a layer, not 1.12's, which has none -// (`0x76a860` takes only region and layer and inserts at the head); stock content never sets it. +// The sub-level is the engine's own ordering within a layer, set only for the EditBox caret: 1.12 +// gives Lua and XML none (`0x76a860` takes only region and layer and inserts at the head, and +// `SetDrawLayer` `0x79a780` reads the layer alone). const STRATUM_SHIFT: u32 = 60; const LEVEL_SHIFT: u32 = 44; @@ -415,8 +413,6 @@ mod tests { let lo = ZKey::frame(Strata::World, u16::MAX, 0); let hi = ZKey::frame(Strata::Tooltip, 0, 0); assert!(hi > lo); - // BLIZZARD is above TOOLTIP. - assert!(ZKey::frame(Strata::Blizzard, 0, 0) > ZKey::frame(Strata::Tooltip, u16::MAX, 0)); } #[test] diff --git a/crates/benilla-ui/src/script/aura/mod.rs b/crates/benilla-ui/src/script/aura/mod.rs index 659c55bf..31760129 100644 --- a/crates/benilla-ui/src/script/aura/mod.rs +++ b/crates/benilla-ui/src/script/aura/mod.rs @@ -5,8 +5,8 @@ //! the list of the unit they name, and a token naming the player reads the player's list in cache //! order, where the reference's `UnitBuff("player", i)` reads by slot. //! -//! `UnitAura`, `UnitBuff`, `UnitDebuff` and `CancelUnitBuff` take a token and a 1-based index into -//! the sign-filtered list; the getters answer nil past the end. The 1.12 `GetPlayerBuff` +//! `UnitBuff` and `UnitDebuff` take a token and a 1-based index into the sign-filtered list and +//! answer nothing past the end. The 1.12 `GetPlayerBuff` //! ([`player_buff`]) takes a 0-based index and returns a cache position, the same number under //! every filter, which its siblings and `GameTooltip:SetPlayerBuff` take in place of the counter. //! @@ -23,7 +23,7 @@ mod player_buff; /// [`crate::script::UiScript::set_player_auras`]). #[derive(Clone, Debug, Default, PartialEq)] pub struct AuraState { - /// `Spell.dbc` id, `UnitAura`'s `spellId`. + /// `Spell.dbc` id, what `CMSG_CANCEL_AURA` names and the app's icon lookup keys on. pub spell_id: u32, /// The spell's name; `None` when the catalog lacks the id. pub name: Option, @@ -64,43 +64,16 @@ pub struct TrackingState { pub cancelable: bool, } -/// `UnitAura`'s `filter`: `|`-separated tokens, the sign defaulting to `HELPFUL` as in the Era API. -struct Filter { - helpful: bool, - cancelable: Option, -} - -impl Filter { - fn parse(spec: Option<&str>) -> Self { - let spec = spec.unwrap_or(""); - let has = |t: &str| spec.split('|').any(|s| s.trim().eq_ignore_ascii_case(t)); - Self { - helpful: !has("HARMFUL"), - cancelable: match (has("CANCELABLE"), has("NOT_CANCELABLE")) { - (true, false) => Some(true), - (false, true) => Some(false), - // Both or neither: no constraint; the reference never passes both. - _ => None, - }, - } - } - - fn matches(&self, a: &AuraState) -> bool { - a.helpful == self.helpful && self.cancelable.is_none_or(|c| a.cancelable == c) - } -} - -/// The cancel gate of `CancelUnitBuff` and `CancelPlayerBuff` (`0x4e49a0`, `0x4e49fb`-`0x4e4a14`): +/// The cancel gate of `CancelPlayerBuff` (`0x4e49a0`, `0x4e49fb`-`0x4e4a14`): /// a helpful aura with `AFLAG_CANCELABLE` (vmangos: unless `SPELL_ATTR_NO_AURA_CANCEL`), or any /// aura of a channeled spell, the only negative aura that cancels (breaking a channel on you). pub(super) fn cancel_authorized(a: &AuraState) -> bool { (a.helpful && a.cancelable) || a.channeled } -/// The 1.12 tuple, not a prefix of the Era one: `UnitBuff` returns `(texture, applications)` -/// (`0x519500`) and `UnitDebuff` adds `dispelType` (`0x5198f0`), as stock `TargetFrame.lua:287-290` -/// reads them. -fn returns_1121(lua: &Lua, a: &AuraState, with_dispel_type: bool) -> mlua::Result { +/// The 1.12 tuple: `UnitBuff` returns `(texture, applications)` (`0x519500`) and `UnitDebuff` adds +/// `dispelType` (`0x5198f0`), as stock `TargetFrame.lua:287-290` reads them. +fn returns(lua: &Lua, a: &AuraState, with_dispel_type: bool) -> mlua::Result { let icon = match &a.icon { Some(t) => Value::String(lua.create_string(t)?), None => Value::Nil, @@ -115,38 +88,6 @@ fn returns_1121(lua: &Lua, a: &AuraState, with_dispel_type: bool) -> mlua::Resul Ok(MultiValue::from_vec(out)) } -/// Which return tuple a getter pushes. -#[derive(Clone, Copy)] -enum Shape { - /// `UnitAura`'s ten values, the Era signature. - Era, - /// `UnitBuff`'s two. - Buff, - /// `UnitDebuff`'s three. - Debuff, -} - -fn returns(lua: &Lua, a: &AuraState) -> mlua::Result { - let s = |v: &Option| -> mlua::Result { - Ok(match v { - Some(t) => Value::String(lua.create_string(t)?), - None => Value::Nil, - }) - }; - Ok(MultiValue::from_vec(vec![ - s(&a.name)?, - s(&a.icon)?, - Value::Integer(i64::from(a.count)), - s(&a.debuff_type)?, - Value::Number(a.duration), - Value::Number(a.expiration_time), - Value::Nil, // source: the 1.12 wire carries no aura caster - Value::Nil, // isStealable: Spellsteal is TBC - Value::Nil, // nameplateShowPersonal - Value::Integer(i64::from(a.spell_id)), - ])) -} - /// The list of the unit `token` names, through the resolver (`0x515970`, which `UnitBuff` calls at /// `0x519542`): the player's cache list for the player, else that guid's; `None` for nobody or a /// unit with no list. A token the resolver does not recognise raises `Unknown unit name`. @@ -161,14 +102,13 @@ pub(crate) fn auras_of<'m>(model: &'m Model, token: &str) -> mlua::Result