From 79e2494fc62e9bc96c3ff3e01a126e8a0b8ac691 Mon Sep 17 00:00:00 2001 From: grom Date: Sun, 1 Nov 2026 14:19:56 -0700 Subject: [PATCH] compat: math.huge and SetColorTexture, the two load blockers of the ported pair MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured through `addon_harness` before anything was written, which is the only reason this is two small changes rather than a guess at a list. Both `Addon_Ports` addons failed to load at all; these are their first two stops. **`math.huge` was nil, deliberately.** `stdlib_backport.rs` restores the 5.1 stdlib members ClassicAPI backports, scoped to what the corpus calls, and `math.huge` was on the stated "nothing calls them" list. `Cinematic/Core.lua:1103` is `local lastMovingOff = -math.huge` at **file scope**, so the nil stopped the addon's main file dead ("attempt to perform arithmetic on field `?`") and nothing after it ran — one site, but a load-time one, which is the bar that table sets. The real `ClassicAPI.dll` carries `huge` in its own stdlib name table beside `fmod`, `modf`, `gmatch`, `match`, `select`, `maxn`, `cosh` and `reverse`, so this is what the client we already claim to be actually has. Two tests pinned it absent and are updated rather than deleted: `lua50.rs`'s keeps asserting this *layer* removes it (the registry stash is the proof), and `stdlib_backport.rs`'s keeps the narrowness assertion for the four that stay gone. The new assertion is `-math.huge`, because file-scope arithmetic is the shape that failed. **`Texture:SetColorTexture`** is the modern spelling of `SetTexture`'s colour arm, which 1.12 already has (`0x770360`'s 8×8 solid surface, and `region/paint.rs` defaults the alpha to 1 as it does). Retail split the names when `SetTexture` stopped taking a colour, so every Era-era addon writes the new one. `Cinematic/Options.lua:701` paints its list border with it and died at session start; OpenMapClassic its map chrome. Four lines of the same composition chunk `SetShown` and the geometry pair use, and an `ALLOWED_BEYOND` row with both call sites — `docs/ADDON_COMPAT.md` had it on the same "plausible and untraced" line as `RegisterUnitEvent`, and this is that line's follow-through. Movement, same harness both ways: - Cinematic load: `Core.lua:1103` → `Frames.lua:2611` (~10 files further; wants `RegisterUnitEvent` next). Session: `Options.lua:701` → `Options.lua:4130` (3400 lines on, wants `SetWordWrap`). - OpenMapClassic: unchanged at `DialogBorderOpaqueTemplate`, which neither fix touches. benilla-ui 2076 passed, 0 failed. Full chain green. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- .../src/script/compat/classic_api/frame.rs | 15 +++++++++ .../compat/classic_api/stdlib_backport.rs | 32 ++++++++++++------- crates/benilla-ui/src/script/lua50.rs | 6 ++-- .../src/script/tests/widget_surface.rs | 9 ++++++ docs/ADDON_COMPAT.md | 2 +- 5 files changed, 49 insertions(+), 15 deletions(-) diff --git a/crates/benilla-ui/src/script/compat/classic_api/frame.rs b/crates/benilla-ui/src/script/compat/classic_api/frame.rs index 0ee8a7fc..3eb150cc 100644 --- a/crates/benilla-ui/src/script/compat/classic_api/frame.rs +++ b/crates/benilla-ui/src/script/compat/classic_api/frame.rs @@ -78,6 +78,20 @@ end end "#; +/// Texture-only: `SetColorTexture(r, g, b[, a])` is the modern name for `SetTexture`'s colour arm, +/// which 1.12 already has (`0x770360` makes the 8x8 solid surface, and our `region/paint.rs` arm +/// defaults the alpha to 1 exactly as it does). Retail split the two spellings when `SetTexture` +/// stopped taking a colour; every Era-era addon writes the new one, and both of the addons being +/// ported here are blocked on it — `Cinematic/Options.lua:701` paints its list border with it, and +/// OpenMapClassic its map chrome. +const TEXTURE_DELEGATES: &str = r#" +return function(m) +function m.SetColorTexture(self, r, g, b, a) + self:SetTexture(r, g, b, a) +end +end +"#; + /// FontString-only: `SetFormattedText` is `SetText(format(...))`. 1.12's `string.format` is the /// `format` global here, as `lua50.rs` leaves it. const FONTSTRING_DELEGATES: &str = r#" @@ -157,6 +171,7 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { delegate(lua, key, GEOMETRY_DELEGATES)?; delegate(lua, key, SHOWN_DELEGATES)?; } + delegate(lua, REG_TEXTURE_METHODS, TEXTURE_DELEGATES)?; delegate(lua, REG_FONTSTRING_METHODS, FONTSTRING_DELEGATES)?; // `GetStringHeight()`: the laid-out height of the current text — wrapped to the declared diff --git a/crates/benilla-ui/src/script/compat/classic_api/stdlib_backport.rs b/crates/benilla-ui/src/script/compat/classic_api/stdlib_backport.rs index e11867fa..094daf80 100644 --- a/crates/benilla-ui/src/script/compat/classic_api/stdlib_backport.rs +++ b/crates/benilla-ui/src/script/compat/classic_api/stdlib_backport.rs @@ -28,13 +28,18 @@ //! | `math.fmod` | 2 | //! | `math.modf` | 1 | //! | `string.reverse` | 1 | +//! | `math.huge` | 1 | //! //! `string.reverse` joined the list when SoftSpoken was installed — it is a ClassicAPI-dependent -//! addon, so it writes 5.1 and its one `string.reverse` site is a load-time call. +//! addon, so it writes 5.1 and its one `string.reverse` site is a load-time call. `math.huge` +//! joined it when the two Addon_Ports addons were measured: `Cinematic/Core.lua:1103` is +//! `local lastMovingOff = -math.huge` at **file scope**, so the nil stopped the addon's main file +//! dead ("attempt to perform arithmetic on field `?`") and nothing after it ran. One site is the +//! bar this table sets, and a load-time one at that. //! //! Deliberately **not** restored, because nothing calls them: `strrev`, -//! `table.maxn`, `math.huge`, the hyperbolics, and `coroutine`. Each stays nil, so the surface -//! grows by five names rather than by a whole library — and `table.maxn` in particular would +//! `table.maxn`, the hyperbolics, and `coroutine`. Each stays nil, so the surface +//! grows by six names rather than by a whole library — and `table.maxn` in particular would //! fight `lua50.rs`'s `n`-based table library, which is 5.0 semantics this client genuinely needs. //! //! ## What this costs, stated plainly @@ -60,6 +65,12 @@ const RESTORED: &[(&str, &str)] = &[ ("math", "fmod"), ("math", "modf"), ("string", "reverse"), + // Not a function: the stash is over `Value`, so a number restores the same way. The real + // `ClassicAPI.dll` carries `huge` in its own stdlib name table beside `fmod`, `modf`, + // `gmatch`, `match`, `select`, `maxn`, `cosh` and `reverse` (its string pool, the evidence + // class `benilla-dll-contract-from-strings` names), so this is what the client it claims to + // be actually has. + ("math", "huge"), ]; pub(super) fn install(lua: &Lua) -> mlua::Result<()> { @@ -138,25 +149,24 @@ mod tests { "abc" ); assert_eq!(s.eval::("return math.fmod(7, 3)").unwrap(), 1.0); + // `math.huge` is a number, not a function: the one site that needed it is file-scope + // arithmetic (`Cinematic/Core.lua:1103`, `-math.huge`), so negating it is the test. + assert!(s + .eval::("return -math.huge < -1e308 and math.huge > 1e308") + .unwrap()); assert_eq!( s.eval::<(f64, f64)>("return math.modf(3.25)").unwrap(), (3.0, 0.25) ); } - /// The narrowness is the point: five names, not a whole library. `table.maxn` especially must + /// The narrowness is the point: six names, not a whole library. `table.maxn` especially must /// stay gone — it fights `lua50.rs`'s `n`-based table library, which is 5.0 semantics this /// client relies on. #[test] fn nothing_beyond_the_measured_members_comes_back() { let s = UiScript::new().unwrap(); - for expr in [ - "strrev", - "table.maxn", - "math.huge", - "math.cosh", - "coroutine", - ] { + for expr in ["strrev", "table.maxn", "math.cosh", "coroutine"] { assert_eq!( s.eval::(&format!("return type({expr})")).unwrap(), "nil", diff --git a/crates/benilla-ui/src/script/lua50.rs b/crates/benilla-ui/src/script/lua50.rs index ff263472..fc02cc83 100644 --- a/crates/benilla-ui/src/script/lua50.rs +++ b/crates/benilla-ui/src/script/lua50.rs @@ -513,17 +513,17 @@ mod tests { } /// The 5.1-only members this *dialect* removes stay removed in the finished VM — except the - /// five `compat/classic_api/stdlib_backport.rs` deliberately hands back, which is a documented + /// six `compat/classic_api/stdlib_backport.rs` deliberately hands back, which is a documented /// maintainer decision, not drift. /// - /// Split in two on purpose. For the restored five, asserting "nil" would be asserting the + /// Split in two on purpose. For the restored six, asserting "nil" would be asserting the /// opposite of the decision; instead this checks they are in the registry stash, which is /// proof *this layer* removed them and only compat put them back. For everything else the /// original assertion stands unchanged, so a genuine regression still fails here. #[test] fn the_5_1_only_members_are_not_offered() { let s = UiScript::new().unwrap(); - for expr in ["table.maxn", "math.huge", "math.cosh", "coroutine"] { + for expr in ["table.maxn", "math.cosh", "coroutine"] { assert!( s.eval::(&format!("return {expr} == nil")).unwrap(), "{expr} is a Lua 5.1 addition — 1.12 does not have it, and nothing restores it" diff --git a/crates/benilla-ui/src/script/tests/widget_surface.rs b/crates/benilla-ui/src/script/tests/widget_surface.rs index 75030135..006b5339 100644 --- a/crates/benilla-ui/src/script/tests/widget_surface.rs +++ b/crates/benilla-ui/src/script/tests/widget_surface.rs @@ -290,6 +290,15 @@ const ALLOWED_BEYOND: &[(&str, &str, &str)] = &[ "ClassicAPI: the laid-out wrapped height, the other half of GetStringWidth's measure — \ AceGUI-3.0's Label and CheckBox call it unguarded", ), + ( + "SetColorTexture", + "Texture", + "ClassicAPI: the modern spelling of SetTexture's colour arm, which 1.12 already has \ + (0x770360's 8x8 solid surface; region/paint.rs defaults the alpha to 1 as it does). \ + Retail split the two names when SetTexture stopped taking a colour, so every Era-era \ + addon writes this one — Cinematic/Options.lua:701 paints its list border with it and \ + died at session start without it, and OpenMapClassic its map chrome", + ), ( "SetShown", "Button CheckButton ColorSelect DressUpModel EditBox FontString Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel Texture WorldFrame", diff --git a/docs/ADDON_COMPAT.md b/docs/ADDON_COMPAT.md index 380c85bb..616efa9b 100644 --- a/docs/ADDON_COMPAT.md +++ b/docs/ADDON_COMPAT.md @@ -251,7 +251,7 @@ separately, so "cannot say" has to be distinguishable from "no". | Combat | partial | `compat/classic_api/misc.rs`. `InCombatLockdown()` → always `false`, and that is the honest answer rather than a stub: the lockdown exists on modern clients to refuse secure-frame edits mid-combat and 1.12 has no secure-template system at all, so nothing here is ever locked down. `StartAttack`/`StopAttack` deliberately not added here — they are 1.12's *own* verbs that benilla hasn't built yet, so registering them from `compat/` would put a stock gap in the wrong layer and hide it from the reference-surface accounting | | Class, Totem, Tracking, Instance | todo, blocked | `FillLocalizedClassList`: the nine localized class names are DBC-resolved app-side (`benilla-app`'s `ui_unit::class_names`) and `benilla-ui` deliberately holds no DBC — the player's own class reaches it through `PlayerRecord`, but not all nine, so this needs a feed like `SelfMovement`'s, not a binding. `Totem` (4 functions): no totem state exists in the model at all; 1.12's own totem bar is unbuilt, so there is nothing to wrap. `Tracking` (3): `Model::tracking` is the one *active* tracking aura (behind the stock `GetTrackingTexture`), not the enumerable list of known tracking spells these index into. `GetInstanceInfo`: `script/instance.rs` feeds only the instance *type*; the modern tuple also wants the instance name, difficulty index and name, and max players — none tracked, and the name would have to be guessed from zone text | | Hooks (`hooksecurefunc`) | done | `compat/classic_api/hooks.rs`, both documented forms (over a global, over a table field). Pure Lua machinery, no game state. The contract it keeps, each covered by its own test: the original runs first with the caller's arguments; **the original's return values pass through untouched** and the hook's are discarded; a hook that errors has its error recorded (`Model::record_script_error`) and the original's returns still come back, while the *original's* own error still propagates; and hooking twice **chains** in install order rather than replacing. A name holding no function is a silent no-op — the reference raises, but an addon written against a client that has the verb it meant to hook is the common case here and taking its UI down over a missing one would be worse. The original and hook live in the Lua registry, since this crate holds no persistent `Function` handle | -| Frame (widget methods) | partial | `compat/classic_api/frame.rs`. These are **methods, not globals**, written straight into the stock class method tables (`__benilla_frame_methods` and the two region leaves) — `object.rs`'s `frame_meta_for` *links* those tables rather than copying them ("a later write to a table shows through"), so a late install from `compat/` reaches every widget already made and every one made later. Built: `SetShown`, `GetSize`/`SetSize`, `GetRect`, `SetFormattedText` (FontString), `HookScript`, `IsEventRegistered`, `GetEffectiveAlpha`, and `SetAttribute`/`SetAttributeNoHandler`/`GetAttribute`/`ClearAttribute`. The geometry/visibility/text ones are composed from the stock verbs as a Lua chunk, so they reuse the stock resolution exactly instead of duplicating it in Rust — note the chunks return a function rather than reading `...`, since this VM is 1.12's Lua **5.0** where `...` is a parameter-list token only, never an expression. Skipped, each with the reason in that file's header: the **mask** and **atlas** families (benilla's UI quad carries one texture and a UV rect — no mask slot, no atlas registry; needs a render-path feature first), the **secure-template** family (`RegisterStateDriver`, `RegisterUnitWatch`, `SecureCmdOptionParse`, `SecureButton_*`, … — modern restricted execution, which 1.12 has no equivalent of; `SetAttribute`/`GetAttribute` ARE built because stripped of secure semantics they are just a per-frame key/value store, and that much is honest, but the drivers that *watch* attributes are the actual subsystem and are not), the **per-line** text-measurement getters (`GetNumLines`, `GetWrappedWidth`, `IsTruncated`, … — need per-line layout facts the host measurer is not asked for today; `measure.rs` answers one whole-string width/height). **`GetStringHeight` was on that skipped list and has been built**, because the blanket reason did not apply to it: the whole-string height is the other half of the number `GetStringWidth` already reads, so `measure::current_measure` now owns the staleness rule for both and the method is a read, not a new host question. AceGUI-3.0 calls it **unguarded** (`AceGUIWidget-Label.lua:54` `height = label:GetStringHeight()`; `AceGUIWidget-CheckBox.lua:90,210` `self:SetHeight(28 + self.desc:GetStringHeight())`), so a nil method there raises and takes the library's own zero-height fallback with it. **Correction, and it matters:** those are the `Libs/` (Mainline), `2.4.3/` and `3.3.5/` library trees AI_VoiceOver ships — **not** the `1.12/` tree its TOC actually loads, which reads `label:GetHeight()` instead and has had every one-argument `SetPoint` rewritten besides. This method is therefore ClassicAPI **parity surface**, not the fix for anything on this install, and the earlier claim here that it explained "can't see the VoiceOver options" was wrong. That symptom was **ours and elsewhere**: the ScrollFrame child re-anchor replaced the child's whole anchor list where 1.12's `SetPoint` merges one point into it, so AceGUI's `TOPLEFT`/`TOPRIGHT`-authored scroll child lost the half that gave it a width and resolved to no rect — and nothing below it resolved either. Fixed in `layout.rs`'s `set_frame_anchored`; measured through the same probe, which went from every node under the scroll child answering `GetLeft() == nil` to a populated, correctly placed tree. Kept because the blanket "needs per-line layout facts" reason genuinely does not apply to it and a 2.4.3/3.3.5 client of this library would need it; what was learned is that an addon shipping several library copies has to be read through its own TOC chain before any call site in it is evidence. Also in this family, each plausible and untraced this pass: `IsMouseOver`/`GetMouseFoci`/`IsDragging`/`SetResizeBounds`/`RegisterUnitEvent`/`SetColorTexture`/desaturation/rotation/the EditBox cursor-and-highlight getters (plausible, untraced this pass). All 12 new methods are registered in `widget_surface.rs`'s `ALLOWED_BEYOND` with their reasons | +| Frame (widget methods) | partial | `compat/classic_api/frame.rs`. These are **methods, not globals**, written straight into the stock class method tables (`__benilla_frame_methods` and the two region leaves) — `object.rs`'s `frame_meta_for` *links* those tables rather than copying them ("a later write to a table shows through"), so a late install from `compat/` reaches every widget already made and every one made later. Built: `SetShown`, `GetSize`/`SetSize`, `GetRect`, `SetFormattedText` (FontString), `HookScript`, `IsEventRegistered`, `GetEffectiveAlpha`, and `SetAttribute`/`SetAttributeNoHandler`/`GetAttribute`/`ClearAttribute`. The geometry/visibility/text ones are composed from the stock verbs as a Lua chunk, so they reuse the stock resolution exactly instead of duplicating it in Rust — note the chunks return a function rather than reading `...`, since this VM is 1.12's Lua **5.0** where `...` is a parameter-list token only, never an expression. Skipped, each with the reason in that file's header: the **mask** and **atlas** families (benilla's UI quad carries one texture and a UV rect — no mask slot, no atlas registry; needs a render-path feature first), the **secure-template** family (`RegisterStateDriver`, `RegisterUnitWatch`, `SecureCmdOptionParse`, `SecureButton_*`, … — modern restricted execution, which 1.12 has no equivalent of; `SetAttribute`/`GetAttribute` ARE built because stripped of secure semantics they are just a per-frame key/value store, and that much is honest, but the drivers that *watch* attributes are the actual subsystem and are not), the **per-line** text-measurement getters (`GetNumLines`, `GetWrappedWidth`, `IsTruncated`, … — need per-line layout facts the host measurer is not asked for today; `measure.rs` answers one whole-string width/height). **`GetStringHeight` was on that skipped list and has been built**, because the blanket reason did not apply to it: the whole-string height is the other half of the number `GetStringWidth` already reads, so `measure::current_measure` now owns the staleness rule for both and the method is a read, not a new host question. AceGUI-3.0 calls it **unguarded** (`AceGUIWidget-Label.lua:54` `height = label:GetStringHeight()`; `AceGUIWidget-CheckBox.lua:90,210` `self:SetHeight(28 + self.desc:GetStringHeight())`), so a nil method there raises and takes the library's own zero-height fallback with it. **Correction, and it matters:** those are the `Libs/` (Mainline), `2.4.3/` and `3.3.5/` library trees AI_VoiceOver ships — **not** the `1.12/` tree its TOC actually loads, which reads `label:GetHeight()` instead and has had every one-argument `SetPoint` rewritten besides. This method is therefore ClassicAPI **parity surface**, not the fix for anything on this install, and the earlier claim here that it explained "can't see the VoiceOver options" was wrong. That symptom was **ours and elsewhere**: the ScrollFrame child re-anchor replaced the child's whole anchor list where 1.12's `SetPoint` merges one point into it, so AceGUI's `TOPLEFT`/`TOPRIGHT`-authored scroll child lost the half that gave it a width and resolved to no rect — and nothing below it resolved either. Fixed in `layout.rs`'s `set_frame_anchored`; measured through the same probe, which went from every node under the scroll child answering `GetLeft() == nil` to a populated, correctly placed tree. Kept because the blanket "needs per-line layout facts" reason genuinely does not apply to it and a 2.4.3/3.3.5 client of this library would need it; what was learned is that an addon shipping several library copies has to be read through its own TOC chain before any call site in it is evidence. **`SetColorTexture` has since been built too**, on the same footing: it is the modern spelling of `SetTexture`'s colour arm, which 1.12 already has (`0x770360`), so it composes in four lines of the same Lua chunk and adds no behaviour. Traced by porting the `Addon_Ports` pair — `Cinematic/Options.lua:701` paints its list border with it and died at session start without it, and OpenMapClassic its map chrome. Also in this family, each plausible and untraced this pass: `IsMouseOver`/`GetMouseFoci`/`IsDragging`/`SetResizeBounds`/`RegisterUnitEvent` (now traced: `Cinematic/Frames.lua:2611` is the next load blocker)/desaturation/rotation/the EditBox cursor-and-highlight getters. All 12 new methods are registered in `widget_surface.rs`'s `ALLOWED_BEYOND` with their reasons | | Container (`C_Container`) | done | `compat/classic_api/container.rs`; almost entirely translation over the stock 1.12 bag verbs' own data (`Model.containers`), not new game logic. `GetContainerItemID/Info`, `HasContainerItem`, `GetContainerItemDurability`, `GetContainerNumFreeSlots`/`GetContainerFreeSlots`, `CalculateTotalNumberOfFreeBagSlots`, `PlayerHasHearthstone`/`UseHearthstone` (scans bags 0-4 for item id 6948), `SwapItems`/`MoveItem`/`AutoStoreItem` (queue the same `ContainerMove`/`BagAutoStore` the stock pickup/place path does), `SortBags`/`SortBankBags` (no-ops — 1.12 has no sort algorithm to run) and the three sort-preference toggle pairs (stored, nothing downstream reads them) are real. Deliberately nil/false, not guessed: `GetContainerItemCharges` (same charge-slot-identity blocker as SuperWoW's), `GetContainerItemRepairCost` (server-only in vanilla), `GetContainerItemQuestInfo`/`GetContainerItemEquipmentSetInfo` (no such tracking exists, and no such feature exists in 1.12 at all for the latter), `IsContainerItemOpenable` (no lockbox flag tracked) | | CreatureInfo (`C_CreatureInfo`) | partial | `compat/classic_api/creature_info.rs`, the 3 of 9 anything installed actually calls. `GetRaceInfo` and `GetFactionInfo` read a **new app→VM feed of the player's own `ChrRaces.dbc`** (`benilla-formats`' `chr_races.rs` → `benilla-app`'s `chr_races.rs` → `UiScript::set_race_info`), built because this crate holds no DBC and a frozen race table gets a modified realm's names wrong: **OctoWoW's row 10 displays "High Elf" where Blizzard's says "Blood Elf"**, while both keep `clientFileString "BloodElf"` for the art paths — the two columns genuinely disagree, so neither can be derived from the other, and a test pins that. The side on `GetFactionInfo` comes from `ui_unit.rs`'s `race_faction_group`, the same answer `UnitFactionGroup("player")` gives, so the two cannot disagree about a race; a non-playable row carries no side and reads nil rather than a guess. **`GetRaceInfo` answering nil past the last race is load-bearing** — pfUI walks `i = 1, 2, 3…` until nil, so an always-non-nil answer hangs the client at load; its own test says so. `GetCreatureID` decodes the template entry out of bits 24-47 of the 16-hex-digit GUID token the rest of `compat/` passes around, accepting **only** the creature high word so a player or pet GUID answers nil instead of a plausible-looking wrong number (the decode is inlined rather than calling `benilla_protocol::guid::entry` — this crate depends on no protocol or engine crate, and one shift is not worth breaking that boundary for). Not built: `GetCreatureInfoByID`, `RequestLoadCreatureByID`, `GetCreatureFamilyInfo`/`IDs`, `GetCreatureTypeInfo`/`IDs` (each needs a creature/family/type table this VM is not fed, and nothing installed asks) and `GetClassInfo` (the class table exists app-side as `ChrClasses` but has no VM feed yet — the same shape of work this row's race feed just did, if an addon ever wants it). With no install the race table stays empty and every reader answers nil, so an addon's race walk finds none and still loads | | Item (`C_Item`) | partial | `compat/classic_api/item.rs`, 37 entries. Modern id-first and `ItemLocation`-keyed readers over the same state the stock item verbs read: `Model::item_templates` (the one `ItemTemplateView` cache every item tooltip renders through — a miss records the same `CMSG_ITEM_QUERY` ask the stock `GetItemInfo` does, so a modern caller warms the one shared cache, not a parallel one), `Model::containers` and `Model::inventory_slots`. **`ItemLocation` is accepted as the plain table addons build** (`{bagID=, slotIndex=}` / `{equipmentSlotIndex=}`): modern WoW's mixin is pure Lua sugar over exactly those two fields and every entry point only reads them, so the table *is* the contract; the mixin constructors themselves belong with `Mixin`/`CreateFromMixins` in the `Lua` namespace and are not faked here. Built: the existence/identity trio, `GetItemData`/`GetItemDataByID` (the upstream's kitchen-sink table, including the `isConjured`/`isOpenable`/`isLootable`/`isWrapper` flag bools off the template's raw `flags`), `GetItemInfoInstant`, the by-id and by-location getter pairs (name, quality, max stack, sell price, set id, inventory type, icon), `GetItemLink`, `GetStackCount`, `GetItemCount` (bags + worn, with the `includeBank` and charge-multiplying `includeUses` flags), `GetItemSpell` (the on-use trigger only — triggers 0/5, not an equip or chance-on-hit effect), `IsUsableItem`/`IsConsumableItem`/`IsEquippableItem`/`IsEquippedItem`/`IsItemOpenable`/`IsBound`, the `IsItemDataCached` pair (which deliberately record **no** ask — polling is not asking), the two inventory-slot name getters, and `UseItemByName`/`EquipItemByName` over the stock container-use queue. Not built, each with the reason in that file's header: the **guid** family (`GetItemGUID`, `GetItemLocation`, `IsItemGUIDInInventory`, …) — nothing in the model keys items by instance guid, and the modern `"Item-Server-…"` string has no source here; the **item-level** family (`GetCurrentItemLevel`, `GetDetailedItemLevelInfo`, `GetAverageItemLevel`) — item level is not in `SMSG_ITEM_QUERY_SINGLE_RESPONSE` and 1.12 exposes no such concept to the client; `GetItemStats`/`GetItemStatDelta` — the modern shape is keyed by `ITEM_MOD_*` *string* names resolved app-side, and returning the raw numeric keys under the modern name would silently mismatch; `GetItemUniqueness` (wants a limit-category pair 1.12 has no counterpart for); the `LockItem`/`UnlockItem` family (`ContainerSlot::locked` is a pending-operation flag the app owns, not a lock Lua may set); and `UseAtCursor`/`UseAtUnit`/`PickupItem`/`RequestLoadItemData*` (each needs an app-drained queue that does not exist yet) |