diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml index 141189fe..f3bca6ed 100644 --- a/crates/benilla-app/assets/ui/OptionsFrame.xml +++ b/crates/benilla-app/assets/ui/OptionsFrame.xml @@ -1212,6 +1212,22 @@ -- UIOptionsFrame.lua:193-227 re-applies saved uvars, so this window has no load ladder. -- OptionsFrameSliders is the stock table (OptionsFrame.lua:24-34), which addons write. + -- Deviation: TurtleWoW's patch-4 replaces OptionsFrame.lua with a 2.0-era file that never + -- defines it, so the rows below restore the 1.12 table verbatim — `OPTIONS_FARCLIP_MIN/MAX` + -- resolved to their 177/777 values (they live in the 1.12 OptionsFrame.lua:1-2, not in + -- GlobalStrings, so a replacement loses them) — for the graphics rows and any addon. + OptionsFrameSliders = { + { text = UI_SCALE, func = "uiscale", minValue = 0.64, maxValue = 1.0, valueStep = 0.01, tooltipText = OPTION_TOOLTIP_UI_SCALE }, + { text = FARCLIP, func = "farclip", minValue = 177, maxValue = 777, valueStep = 60, tooltipText = OPTION_TOOLTIP_FARCLIP }, + { text = ENVIRONMENT_DETAIL, func = "WorldDetail", minValue = 0, maxValue = 2, valueStep = 1, tooltipText = OPTION_TOOLTIP_ENVIRONMENT_DETAIL }, + { text = TERRAIN_MIP, func = "TerrainMip", minValue = 0, maxValue = 1, valueStep = 1, tooltipText = OPTION_TOOLTIP_TERRAIN_TEXTURE, restartClient = 1, tooltipRequirement = OPTION_RESTART_REQUIREMENT }, + { text = TEXTURE_DETAIL, func = "BaseMip", minValue = 0, maxValue = 1, valueStep = 1, tooltipText = OPTION_TOOLTIP_TEXTURE_DETAIL }, + { text = GAMMA, func = "Gamma", cvar1 = "gamma", minValue = -0.5, maxValue = 0.5, valueStep = 0.1, tooltipText = OPTION_TOOLTIP_GAMMA }, + { text = ANISOTROPIC, func = "anisotropic", minValue = 1, maxValue = 4, valueStep = 1, tooltipText = OPTION_TOOLTIP_ANISOTROPIC, restartClient = 1, tooltipRequirement = OPTION_RESTART_REQUIREMENT }, + { text = SPELL_DETAIL, func = "spellEffectLevel", minValue = 0, maxValue = 2, valueStep = 1, tooltipText = OPTION_TOOLTIP_SPELL_DETAIL }, + { text = WEATHER_DETAIL, func = "weatherDensity", minValue = 0, maxValue = 3, valueStep = 1, tooltipText = OPTION_TOOLTIP_WEATHER_DETAIL }, + } + -- Holes at 4, 5, 7 and 8, so the OnShow loop uses the pairs form: ipairs would stop at 3. BENILLA_GRAPHICS_SLIDER_ROWS = { "RowUiScale", "RowFarclip", "RowWorldDetail", diff --git a/crates/benilla-app/src/ui_unit.rs b/crates/benilla-app/src/ui_unit.rs index fa844a85..6bbc4100 100644 --- a/crates/benilla-app/src/ui_unit.rs +++ b/crates/benilla-app/src/ui_unit.rs @@ -476,6 +476,13 @@ fn fire_unit_combat( /// The race id to `UnitRace`'s display name and `raceFile` token (`"Scourge"`, `"NightElf"`); the /// display name is also what `$R`/`$r` expand to. +/// +/// Deviation: races 9 (Goblin) and 10 (High Elf) are TurtleWoW's additions, absent from stock +/// 1.12's eight; they resolve because the reference reads `ChrRaces.dbc` at call time and +/// TurtleWoW's patched copy carries the rows, so a 1.12 client on that chain answers them too. +/// Without the rows `UnitRace("player")` answers nil and TurtleWoW's `PaperDollFrame_SetLevel` +/// raises in its three-spec `PLAYER_LEVEL` format. (The same justification as the `wipe`/`trim`/ +/// `strsplit` additions in the UI stdlib.) pub(crate) fn race_names(race: u8) -> Option<(&'static str, &'static str)> { Some(match race { 1 => ("Human", "Human"), @@ -486,6 +493,8 @@ pub(crate) fn race_names(race: u8) -> Option<(&'static str, &'static str)> { 6 => ("Tauren", "Tauren"), 7 => ("Gnome", "Gnome"), 8 => ("Troll", "Troll"), + 9 => ("Goblin", "Goblin"), + 10 => ("High Elf", "High Elf"), _ => return None, }) } @@ -1614,6 +1623,25 @@ fn combo_edge(last: Option<(u8, u64)>, now: (u8, u64)) -> Option { mod tests { use super::*; + /// TurtleWoW's added races resolve like the shipped eight: `UnitRace("player")` would return + /// nil without them, and its `PaperDollFrame_SetLevel` (`PLAYER_LEVEL = "Level %d %s %s"`) + /// raises on a nil race in `string.format`. + #[test] + fn turtlewow_races_nine_and_ten_resolve() { + assert_eq!( + race_names(9), + Some(("Goblin", "Goblin")), + "race 9 is TurtleWoW's Goblin" + ); + assert_eq!( + race_names(10), + Some(("High Elf", "High Elf")), + "race 10 is TurtleWoW's High Elf" + ); + assert_eq!(race_names(0), None, "race 0 has no ChrRaces row"); + assert_eq!(race_names(11), None, "race 11 has no ChrRaces row"); + } + /// The team digit comes off the race byte, whatever template sits beside it (a GM's 35). #[test] fn the_team_digit_comes_off_the_race_byte_not_the_faction_template() { diff --git a/crates/benilla-assets/src/lib.rs b/crates/benilla-assets/src/lib.rs index 8c30cd36..08612a3e 100644 --- a/crates/benilla-assets/src/lib.rs +++ b/crates/benilla-assets/src/lib.rs @@ -131,6 +131,16 @@ const SAMPLER_MARKER: char = '@'; /// the extension (`…/leaves01@cc.blp`), which must stay `.blp` to pick the loader. pub fn texture_url(internal: &str, wrap: (bool, bool)) -> String { let path = internal.replace('\\', "/").to_ascii_lowercase(); + // The reference's texture create appends `.blp` to a filename that lacks an extension + // (`CImageManager::CreateTexture`), and TurtleWoW's M2s ship some texture names bare + // (`redtreecanopy_leaves01`, whose `.blp` the chain holds); without the extension the archive + // lookup misses and Bevy cannot pick the BLP loader. + let file = path.rsplit('/').next().unwrap_or(&path); + let path = if file.contains('.') { + path + } else { + format!("{path}.blp") + }; if wrap == (true, true) { return format!("mpq://{path}"); } @@ -257,6 +267,24 @@ mod tests { assert_eq!(strip_sampler_marker("world/foo.blp"), None); assert_eq!(strip_sampler_marker("world/foo@bar.blp"), None); assert_eq!(sampler_mode_of("world/foo@bar.blp"), (true, true)); + + // A TurtleWoW M2 names some textures bare; the reference appends `.blp`, and the marker + // must still land before the appended extension so the loader picks it. + let bare = texture_url( + "World\\Azeroth\\Redridge\\PassiveDoodads\\Trees\\RedTreeCanopy_Leaves01", + (true, true), + ); + assert_eq!( + bare, + "mpq://world/azeroth/redridge/passivedoodads/trees/redtreecanopy_leaves01.blp" + ); + assert_eq!( + texture_url( + "World\\Azeroth\\Redridge\\PassiveDoodads\\Trees\\RedTreeCanopy_Base02", + (false, false), + ), + "mpq://world/azeroth/redridge/passivedoodads/trees/redtreecanopy_base02@cc.blp" + ); } #[test] diff --git a/crates/benilla-ui/src/script/stdlib.rs b/crates/benilla-ui/src/script/stdlib.rs index 5711fb1b..dd24e42e 100644 --- a/crates/benilla-ui/src/script/stdlib.rs +++ b/crates/benilla-ui/src/script/stdlib.rs @@ -286,8 +286,8 @@ __benilla_game_hour = 0 __benilla_game_minute = 0 function GetGameTime() return __benilla_game_hour, __benilla_game_minute end --- ── Beyond 1.12: wipe / strsplit / strjoin / trim ───────────────────────────────────────────── --- Four names 1.12 does NOT have: absent from `reference/1.12-globals.tsv`, absent from the stock +-- ── Beyond 1.12: wipe / strsplit / strjoin / trim / explode ─────────────────────────────────── +-- Names 1.12 does NOT have: absent from `reference/1.12-globals.tsv`, absent from the stock -- chain (no stock `Interface\FrameXML` file calls any of them — `UIParent.lua`, `ChatFrame.lua`, -- `MainMenuBar.lua` and `FriendsFrame.lua` read clean), and absent from every registrar table. They -- are here anyway because a MODIFIED 1.12 UI ships them: TurtleWoW's `patch-4.mpq` replaces @@ -303,15 +303,52 @@ 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 `strsplit` splits on PLAIN text. +-- `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. do - local _find, _sub, _insert, _concat = string.find, string.sub, table.insert, table.concat + 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 for k in pairs(t) do t[k] = nil end end + -- `explode(subject, sep [, out])`: 2.0-era split into a TABLE, unlike `strsplit`'s vararg + -- shape, appending into `out` when given. TurtleWoW's `HonorFrame.lua:46` parses its + -- `TW_HONOR` addon message with `explode(arg2, ADDON_MSG_FIELD_DELIMITER)` and its + -- `WorldStateFrame.lua:233-245` reuses the same `out` tables across calls. A nil subject or + -- separator comes back whole, so a missing delimiter global cannot take the UI down. + function explode(str, sep, out) + out = out or {} + if str == nil then return out end + if sep == nil or sep == "" then + _insert(out, str) + return out + end + local pos = 1 + local sep_len = _len(sep) + while 1 do + local e = _find(str, sep, pos, true) + if e == nil then + _insert(out, _sub(str, pos)) + break + end + _insert(out, _sub(str, pos, e - 1)) + pos = e + sep_len + end + return out + end + + -- TurtleWoW's addon-message field delimiters (`TW_HONOR`, `TW_BG`), named where its + -- `HonorFrame.lua:46` and `WorldStateFrame.lua:331-335` split addon payloads. The values are + -- the same `;`/`:`/`|` its `WorldStateFrame.lua:233-245` writes literally for the fields, + -- arrays and subfields of the same protocol. + ADDON_MSG_FIELD_DELIMITER = ";" + ADDON_MSG_ARRAY_DELIMITER = ":" + ADDON_MSG_SUBFIELD_DELIMITER = "|" + function strsplit(delimiter, subject) -- 5.0's `find` answers an empty plain pattern as `(from, from - 1)`, a zero-width match that -- never advances `from`; looping on it fills the table until it overflows. There is nothing diff --git a/crates/benilla-ui/src/script/tests/reference_surface.rs b/crates/benilla-ui/src/script/tests/reference_surface.rs index d3fff0f5..9248c97d 100644 --- a/crates/benilla-ui/src/script/tests/reference_surface.rs +++ b/crates/benilla-ui/src/script/tests/reference_surface.rs @@ -43,6 +43,12 @@ fn allowed_beyond_1_12() -> HashSet<&'static str> { "strsplit", "strjoin", "trim", + // TurtleWoW's addon-message split and delimiters: its `HonorFrame.lua:46` and + // `WorldStateFrame.lua:233-335` parse the `TW_HONOR`/`TW_BG` addon payloads with them. + "explode", + "ADDON_MSG_FIELD_DELIMITER", + "ADDON_MSG_ARRAY_DELIMITER", + "ADDON_MSG_SUBFIELD_DELIMITER", // ── 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. diff --git a/crates/benilla-ui/src/script/worldmap.rs b/crates/benilla-ui/src/script/worldmap.rs index 1e5a10d2..4d51213d 100644 --- a/crates/benilla-ui/src/script/worldmap.rs +++ b/crates/benilla-ui/src/script/worldmap.rs @@ -348,11 +348,22 @@ fn revealed_overlays(state: &WorldMapState) -> Vec<&WorldMapOverlayView> { impl super::UiScript { /// Pushes the catalog at startup, queueing `WORLD_MAP_UPDATE` so an open map repaints. pub fn set_world_map_catalog(&mut self, continents: Vec) { - let mut model = self.model_mut(); - model.worldmap.continents = continents; - model - .pending_events - .push(("WORLD_MAP_UPDATE".to_string(), Vec::new())); + let n = continents.len(); + { + let mut model = self.model_mut(); + model.worldmap.continents = continents; + model + .pending_events + .push(("WORLD_MAP_UPDATE".to_string(), Vec::new())); + } + // TurtleWoW's WorldMapFrame reads `CONTINENTS_LENGTH` as the continent dropdown's length + // (line 457), so it must be set before the WORLD_MAP_UPDATE above runs the dropdown's + // OnEvent — a global its patch-4 WorldMapFrame.lua expects that the stock client never + // shipped. + self.lua + .globals() + .set("CONTINENTS_LENGTH", n as i64) + .expect("CONTINENTS_LENGTH is a number"); } /// Pushes the orphan list beside the catalog, both filled in one walk (`0x4a5d00`).