diff --git a/crates/benilla-app/src/ui_script/manifest.rs b/crates/benilla-app/src/ui_script/manifest.rs index 5c5edf82..eced1721 100644 --- a/crates/benilla-app/src/ui_script/manifest.rs +++ b/crates/benilla-app/src/ui_script/manifest.rs @@ -71,6 +71,10 @@ fn bootstrap_positions(script: &UiScript) -> Vec { error!("ui_script: focus slash commands: {e}"); return vec![format!("focus slash commands: {e}")]; } + if let Err(e) = install_font_colour_objects(script) { + error!("ui_script: font colour objects: {e}"); + return vec![format!("font colour objects: {e}")]; + } Vec::new() } @@ -109,6 +113,65 @@ if SlashCmdList then end "#; +/// ClassicAPI (`docs/ADDON_COMPAT.md`): 1.12's `*_FONT_COLOR` globals answer `ColorMixin`'s +/// methods, and `WHITE_FONT_COLOR` exists at all. +/// +/// 1.12 ships these as loose `{ r, g, b }` tables (`Fonts.xml`); every client from Legion on +/// makes them `ColorMixin` objects, and addon code written in that dialect calls methods on them +/// rather than reading the fields. pfUI does, in both directions at once: +/// +/// - `modules/socialmod.lua:145` colours each Who row with `NORMAL_FONT_COLOR:GetRGB()`, and +/// `modules/loot.lua` does the same five times for the master-loot menu. grom's session carried +/// **26** `attempt to call method 'GetRGB' (a nil value)` raises, every one of them from opening +/// the Who tab — the list drew uncoloured and each row cost a traceback. +/// - `modules/totems.lua:22` and `modules/minimap.lua:198` call `WHITE_FONT_COLOR:GetRGB()` and +/// `:GetRGBA()`, and 1.12 has no such global at all, so those index a nil. +/// +/// Both are a one-line gap in the same place, so both are closed here. +/// +/// **A metatable, not `Mixin`.** Upstream's `CreateColor` copies the methods in as fields, which is +/// right for an object it owns. These tables are not ours: FrameXML built them, FrameXML reads +/// them, and a 1.12 addon is free to walk one with `pairs`. `__index` leaves `pairs(NORMAL_FONT_COLOR)` +/// answering exactly the three keys it answers on the reference while the methods still resolve. +/// `a` is the one field added, because `GetRGBA` has to answer something and modern WoW's own +/// `NORMAL_FONT_COLOR` is opaque; `.a` on a 1.12 colour table is nil, and nothing in the stock UI +/// reads it. +/// +/// Here rather than in `benilla-ui`'s `compat/classic_api/color_mixin.rs` for the same mechanical +/// reason as [`install_focus_slash_commands`]: the tables do not exist until `Fonts.xml` has run, +/// and the compat layer installs before FrameXML loads. +pub(super) fn install_font_colour_objects(script: &UiScript) -> Result<(), String> { + script.run(FONT_COLOUR_OBJECTS).map_err(|e| e.to_string()) +} + +/// The `*_FONT_COLOR` tables 1.12 declares (`reference/1.12-globals.tsv`), named one by one: a +/// sweep of `_G` for a key ending in `_FONT_COLOR` would also catch an addon's own table of the +/// same name and give it a metatable it did not ask for. +const FONT_COLOUR_OBJECTS: &str = r#" +if ColorMixin then + local meta = { __index = ColorMixin } + local function upgrade(c) + -- An existing metatable is somebody's own arrangement; leave it be. + if type(c) ~= "table" or getmetatable(c) then return c end + if c.a == nil then c.a = 1 end + setmetatable(c, meta) + return c + end + + upgrade(NORMAL_FONT_COLOR) + upgrade(HIGHLIGHT_FONT_COLOR) + upgrade(GRAY_FONT_COLOR) + upgrade(GREEN_FONT_COLOR) + upgrade(RED_FONT_COLOR) + upgrade(PASSIVE_SPELL_FONT_COLOR) + + -- Not a 1.12 global: modern WoW's `WHITE_FONT_COLOR = CreateColor(1, 1, 1)`. + if not WHITE_FONT_COLOR then + WHITE_FONT_COLOR = upgrade({ r = 1, g = 1, b = 1 }) + end +end +"#; + /// SuperWoW (`docs/ADDON_COMPAT.md`): the macro body limit is 255 letters in 1.12 and 510 under /// SuperWoW. [`benilla_ui::script::MAX_MACRO_BODY`] already stores the raised cap; two things in /// the stock UI still carry the old number from the player's own install and need resetting at @@ -351,6 +414,76 @@ pub(crate) fn load_ingame_ui( mod tests { use super::*; + /// pfUI's Who list and master-loot menu colour their rows with `NORMAL_FONT_COLOR:GetRGB()`, + /// and its totem tooltip and minimap zone label with `WHITE_FONT_COLOR`'s. 1.12 has loose + /// tables for the first and no global at all for the second, which is 26 raises in one of + /// grom's sessions, all of them from opening the Who tab. + #[test] + fn the_font_colour_globals_answer_colormixin_methods() { + let s = UiScript::new().unwrap(); + // The shape `Fonts.xml` leaves behind. + s.run("NORMAL_FONT_COLOR = { r = 1, g = 0.82, b = 0 }") + .unwrap(); + install_font_colour_objects(&s).unwrap(); + + assert_eq!( + s.eval::<(f64, f64, f64)>("return NORMAL_FONT_COLOR:GetRGB()") + .unwrap(), + (1.0, 0.82, 0.0), + "pfUI `modules/socialmod.lua:145`" + ); + assert_eq!( + s.eval::<(f64, f64, f64, f64)>("return WHITE_FONT_COLOR:GetRGBA()") + .unwrap(), + (1.0, 1.0, 1.0, 1.0), + "pfUI `modules/minimap.lua:198`; 1.12 has no such global, so this one is ours" + ); + + // The fields FrameXML itself reads are untouched, and `pairs` still answers the keys a + // 1.12 addon expects — which is why this is a metatable and not a `Mixin` copy. + assert_eq!( + s.eval::("return NORMAL_FONT_COLOR.g").unwrap(), + 0.82, + "`OptionsFrameAdapters.lua` reads `.r/.g/.b` directly" + ); + let mut keys: Vec = s + .eval::>( + "local t = {} for k in pairs(NORMAL_FONT_COLOR) do table.insert(t, k) end return t", + ) + .unwrap(); + keys.sort(); + assert_eq!( + keys, + vec!["a", "b", "g", "r"], + "no methods leak into `pairs`" + ); + } + + /// A table that already carries a metatable is somebody else's object, and a second run of + /// the patch (a `ReloadUI`) must not stack anything. + #[test] + fn upgrading_a_colour_table_leaves_an_existing_metatable_alone() { + let s = UiScript::new().unwrap(); + s.run( + "NORMAL_FONT_COLOR = setmetatable({ r = 1, g = 1, b = 1 }, \ + { __index = function() return \"mine\" end })", + ) + .unwrap(); + install_font_colour_objects(&s).unwrap(); + assert_eq!( + s.eval::("return NORMAL_FONT_COLOR.anything") + .unwrap(), + "mine", + "an existing metatable is left as it was found" + ); + assert!( + s.eval::("return rawget(NORMAL_FONT_COLOR, \"a\") == nil") + .unwrap(), + "and no field is added to it either (rawget: this table's own `__index` answers \ + every key)" + ); + } + /// ClassicAPI's `/focus` and `/clearfocus` (`docs/ADDON_COMPAT.md`): registered into the /// table the stock `ChatFrame.lua` declares, and reachable by the same walk every typed or /// macro line takes ([`UiScript::run_slash_command`]).