TurtleWoW compat: restore OptionsFrameSliders, resolve races 9/10, append .blp to bare M2 textures, nil-safe wipe, explode + addon delimiters, CONTINENTS_LENGTH

This commit is contained in:
chairmanpow
2026-10-01 22:52:42 +01:00
parent 031acdd8bb
commit f573d1ad67
6 changed files with 135 additions and 9 deletions
@@ -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",
+28
View File
@@ -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<bool> {
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() {
+28
View File
@@ -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]
+41 -4
View File
@@ -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
@@ -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.
+16 -5
View File
@@ -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<WorldMapContinentView>) {
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`).