From 3247bc7631738e49c386a3d9355cde6da402d32a Mon Sep 17 00:00:00 2001 From: chairmanpow Date: Fri, 2 Oct 2026 13:36:36 +0100 Subject: [PATCH] TurtleWoW compat: attach on the display model, sides for races 9/10, table-returning wipe, no-op arena request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four fixes a TurtleWoW client needs and stock 1.12.1 never asks for, each for a raise it provokes: A player attaches as soon as its display model is ready. Gating on `Equipment::settled` withheld a body for good on a chain that never answers one `PLAYER_VISIBLE_ITEM` template — in a battleground crowd, every other player. 1.12.1 withholds a unit only until its first composite (`0x477860`); the compositor paints the armour it knows and re-dresses when a template lands. Races 9 (Goblin) and 10 (High Elf) get their sides, from TurtleWoW's `FactionTemplate.dbc` group masks (Goblin's template 1609 is mask 5, Horde; High Elf's 1610 is mask 3, Alliance). Without a side, the roster seat's `UnitFactionGroup("player")` is nil and an addon that concatenates it at file scope raises and loses its whole load phase. `wipe` hands back a table for nil. `Blizzard_RaidUI.lua:77,116` clears its `RAID_SUBGROUP_LISTS` with `t[i] = wipe(t[i])` and then `tinsert`s into the result, so a never-populated entry must come back insertable. `RequestArenaScoreInfo` is defined as a no-op. `WorldStateFrame.lua:217` calls it on the battleground score events; the function is native to TurtleWoW's client and the reply is parsed by its own native code, never by that Lua. 1.12.1 has no arenas, so there is no scoreboard here for it to fill. --- crates/benilla-app/src/entities/attach/mod.rs | 10 +++--- crates/benilla-app/src/ui_unit.rs | 31 +++++++++++++++---- crates/benilla-ui/src/script/stdlib.rs | 20 +++++++++--- .../src/script/tests/reference_surface.rs | 3 ++ 4 files changed, 50 insertions(+), 14 deletions(-) diff --git a/crates/benilla-app/src/entities/attach/mod.rs b/crates/benilla-app/src/entities/attach/mod.rs index b6c4d7e0..a4a04b53 100644 --- a/crates/benilla-app/src/entities/attach/mod.rs +++ b/crates/benilla-app/src/entities/attach/mod.rs @@ -280,10 +280,12 @@ pub(super) fn attach_entity_visuals( ) = skin_build; let now = time.elapsed_secs(); for (entity, net, equipment, reattached, mount_child, mount_body, anchored) in &pending { - // A player attaches once its equipment settles, so it never flashes naked. - if net.kind == EntityKind::Player && !equipment.is_some_and(|e| e.settled) { - continue; - } + // A player attaches as soon as its display model is ready. 1.12.1 withholds a unit only + // until its first composite (`0x477860`) is done, never until every `PLAYER_VISIBLE_ITEM` + // template is answered: the compositor paints the armour it knows and re-dresses when a + // template lands (`redress_player_looks`). Gating on `Equipment::settled` here withheld a + // body for good on a chain that never answers one visible-item query — in a battleground + // crowd that is every other player. // A corpse waits for `resolve_corpse_equipment`: it is never re-dressed, so a body // composited early would stay naked. if net.kind == EntityKind::Corpse && equipment.is_none() { diff --git a/crates/benilla-app/src/ui_unit.rs b/crates/benilla-app/src/ui_unit.rs index 6bbc4100..a4e2ae89 100644 --- a/crates/benilla-app/src/ui_unit.rs +++ b/crates/benilla-app/src/ui_unit.rs @@ -518,15 +518,24 @@ pub(crate) fn class_names(class: u8) -> Option<(&'static str, &'static str)> { /// A playable race's fixed side, for where no faction template is at hand, as at world entry, /// where addons concatenate `UnitFactionGroup("player")` at file scope; [`faction_group`] reads /// the live template. +/// +/// Deviation: races 9 (Goblin) and 10 (High Elf) are TurtleWoW's additions, absent from stock +/// 1.12's eight; their sides come from TurtleWoW's `FactionTemplate.dbc` group masks (Goblin's +/// template 1609 is mask 5, Horde; High Elf's 1610 is mask 3, Alliance). Without a side here the +/// roster seat's `UnitFactionGroup("player")` is nil, and a 2.0-era addon that concatenates it at +/// load (aux-addon's `history.lua` `format('%s|%s', realm, UnitFactionGroup'player')`) raises and +/// loses its whole load phase. pub(crate) fn race_faction_group(race: u8) -> Option<&'static str> { - if !(1..=8).contains(&race) { + if !(1..=10).contains(&race) { return None; } - Some(if crate::char_create::ALLIANCE.contains(&race) { - "Alliance" - } else { - "Horde" - }) + Some( + if crate::char_create::ALLIANCE.contains(&race) || race == 10 { + "Alliance" + } else { + "Horde" + }, + ) } /// A unit's PvP team digit, `0x5efe00`'s `0` Horde, `1` Alliance, `-1` no side, from the race and @@ -1640,6 +1649,16 @@ mod tests { ); assert_eq!(race_names(0), None, "race 0 has no ChrRaces row"); assert_eq!(race_names(11), None, "race 11 has no ChrRaces row"); + // Sides for the roster seat's `UnitFactionGroup("player")`, per TurtleWoW's + // `FactionTemplate` group masks (Goblin 5, High Elf 3). + assert_eq!(race_faction_group(9), Some("Horde"), "race 9 is Goblin"); + assert_eq!( + race_faction_group(10), + Some("Alliance"), + "race 10 is High Elf" + ); + assert_eq!(race_faction_group(0), None, "race 0 has no side"); + assert_eq!(race_faction_group(11), None, "race 11 has no side"); } /// The team digit comes off the race byte, whatever template sits beside it (a GM's 35). diff --git a/crates/benilla-ui/src/script/stdlib.rs b/crates/benilla-ui/src/script/stdlib.rs index dd24e42e..8fd77600 100644 --- a/crates/benilla-ui/src/script/stdlib.rs +++ b/crates/benilla-ui/src/script/stdlib.rs @@ -303,16 +303,20 @@ function GetGameTime() return __benilla_game_hour, __benilla_game_minute end -- -- This is a deviation from the reference, kept because 1.12's own UI cannot load without it. The -- shapes are 2.0's, not 1.12's, and each is scoped to what the reference-corpus callers use: --- `wipe` is single-argument (no corpus caller passes a key) and no-ops on nil — TurtleWoW's --- `Blizzard_RaidUI.lua:77` wipes `RAID_SUBGROUP_LISTS` entries that may never be populated, and --- `strsplit` splits on PLAIN text. +-- `wipe` is single-argument (no corpus caller passes a key) and hands back a table for nil — +-- TurtleWoW's `Blizzard_RaidUI.lua:77` clears `RAID_SUBGROUP_LISTS` entries with `= wipe(t[i])` +-- and then `tinsert`s into them, and `strsplit` splits on PLAIN text. do local _find, _sub, _insert, _concat, _len = string.find, string.sub, table.insert, table.concat, string.len local _gsub = string.gsub function wipe(t) - if t == nil then return end + -- `t = t or {}` and a returned table: TurtleWoW's `Blizzard_RaidUI.lua:77,116` clears its + -- `RAID_SUBGROUP_LISTS` with `t[i] = wipe(t[i])` and then `tinsert`s into it, so `wipe` of + -- a never-populated entry must hand back a table to insert into. + t = t or {} for k in pairs(t) do t[k] = nil end + return t end -- `explode(subject, sep [, out])`: 2.0-era split into a TABLE, unlike `strsplit`'s vararg @@ -393,6 +397,14 @@ end -- define the name themselves under their own namespace. An addon reaching for one of these is -- reaching for a later client's API, and it should find what it would find there: nothing. +-- ── TurtleWoW's arena-score request ──────────────────────────────────────────────────────────── +-- TurtleWoW's `WorldStateFrame.lua:217` calls `RequestArenaScoreInfo()` on the battleground score +-- events. The function is native to its client — the reply fills `this.arenaData`, which its Lua +-- only reads — and benilla has no arena scoreboard (1.12 has no arenas, and the reply is parsed +-- by TurtleWoW's native code, never this Lua). So the request is a no-op that keeps the frame +-- from raising; `RequestCustomScoreInfo`, the battleground twin, is defined by the file itself. +RequestArenaScoreInfo = function() end + -- ── Blizzard's positional string.format (%N$) ────────────────────────────────────────────────── do -- `_find`/`_sub` are captured for the same reason `_format` is: this wrapper runs on every diff --git a/crates/benilla-ui/src/script/tests/reference_surface.rs b/crates/benilla-ui/src/script/tests/reference_surface.rs index 9248c97d..f8d10286 100644 --- a/crates/benilla-ui/src/script/tests/reference_surface.rs +++ b/crates/benilla-ui/src/script/tests/reference_surface.rs @@ -49,6 +49,9 @@ fn allowed_beyond_1_12() -> HashSet<&'static str> { "ADDON_MSG_FIELD_DELIMITER", "ADDON_MSG_ARRAY_DELIMITER", "ADDON_MSG_SUBFIELD_DELIMITER", + // TurtleWoW's native arena-score request: a no-op here (its reply is parsed by TurtleWoW's + // client, and benilla has no arena scoreboard), so its WorldStateFrame.lua:217 cannot raise. + "RequestArenaScoreInfo", // ── WoW API past 1.12 ── // Only `SubmitChatInput` is called from our UI files (`ScriptLogFrame.xml`'s slash // commands); each of the rest awaits its 1.12 equivalent, removal or a reason.