compat: RegisterUnitEvent, the fixed-strata pair and SetWordWrap
The next three blockers of the `Addon_Ports` pair, each measured with `addon_harness` before and after rather than picked off a list. **`RegisterUnitEvent(event, unit1 [, unit2])`** — Cinematic's load blocker, and `docs/ADDON_COMPAT.md` already had it on the "plausible and untraced" line. The registration is the stock one: same `event_to_frames` list, same registration order, and `Model::frame_unit_events` holds the units per frame per event. `tick.rs`'s walk skips a frame whose row for this event does not name `arg1`; the map is empty on an install with no such addon and the walk tests `is_empty()` first, so the common path costs one load. All three unregister paths drop the row, including the one that is easy to miss: a plain `RegisterEvent` after a unit register **widens** the registration back, as upstream's does — the frame keeps its place and loses the filter. A row surviving an unregister/re-register cycle would swallow that event forever, so the test walks the whole cycle. `Cinematic/Frames.lua:2611` registers `UNIT_AURA` for `"player"` at file scope, so the nil method stopped the file dead. **`SetFixedFrameStrata`/`SetFixedFrameLevel`** are no-ops, and that is the honest answer rather than a stub: 9.0 added them to tell the *frame manager* to stop re-deciding a frame's strata and level, and this client has no such manager — both are whatever the setters last wrote and nothing else rewrites them, so the guarantee the caller asks for already holds. Ace3 calls both unguarded (`AceConfigDialog-3.0.lua:575-576`). **`SetWordWrap`/`CanWordWrap`** is 3.x's whether-to-wrap switch; 1.12's `SetNonSpaceWrap` is a different question (where a wrap may fall, not whether one happens). **Stored, not applied**, on the footing `SetSpacing` and `non_space_wrap` already set in `types.rs`: the text layout wraps to the declared width unconditionally and has no one-line-and-clip mode for the flag to drive, so honouring it would mean inventing a layout mode. Kept because the pair must round-trip and because the missing *setter* is what costs — `Cinematic/Options.lua:4130` calls it on a label and aborted the options panel there. All four are registered in `widget_surface.rs`'s `ALLOWED_BEYOND` with their call sites. The first `RegisterUnitEvent` row said "Frame" and the wall rejected it: event registration lives on the one shared frame method table, which is where 1.12 keeps `RegisterEvent` too, so the two are registered alike on all twenty frame kinds or neither is. Movement: Cinematic's load `Frames.lua:2611` → `CameraWheel.lua:243`, session `Options.lua:4130` → clear; openmap `AceConfigDialog:573` → `:575`. benilla-ui 2077 passed, 0 failed. Full chain green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -198,6 +198,40 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
.unwrap_or(0.0))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// `SetWordWrap(on)` / `CanWordWrap()`: 3.x's switch for whether a FontString wraps at all.
|
||||
// 1.12's own `SetNonSpaceWrap` is a different question (where a wrap may fall, not whether
|
||||
// one happens), so there is nothing to spell this as.
|
||||
//
|
||||
// **Stored, not applied**, on the same footing as `SetSpacing` and for the same stated
|
||||
// reason: this client's text layout wraps to the declared width unconditionally and has no
|
||||
// one-line-and-clip mode for the flag to drive, so honouring it would be inventing a
|
||||
// layout mode, while answering nothing costs an addon its whole build —
|
||||
// `Cinematic/Options.lua:4130` calls it on a label and aborted the options panel there.
|
||||
// The pair round-trips, so a caller that reads back what it wrote gets its own answer.
|
||||
fs.set(
|
||||
"SetWordWrap",
|
||||
lua.create_function(|lua, (this, on): (Table, Option<Value>)| {
|
||||
let rh = crate::script::region::region_handle_of(lua, &this)?;
|
||||
let on = !matches!(on, None | Some(Value::Nil) | Some(Value::Boolean(false)));
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
model.region_data.entry(rh).or_default().word_wrap = Some(on);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
fs.set(
|
||||
"CanWordWrap",
|
||||
lua.create_function(|lua, this: Table| {
|
||||
let rh = crate::script::region::region_handle_of(lua, &this)?;
|
||||
let model = lua.app_data_ref::<Model>().expect("model app_data");
|
||||
let on = model
|
||||
.region_data
|
||||
.get(&rh)
|
||||
.and_then(|d| d.word_wrap)
|
||||
.unwrap_or(true);
|
||||
Ok(crate::script::binding_abi::flag(on))
|
||||
})?,
|
||||
)?;
|
||||
}
|
||||
|
||||
let m: Table = lua.named_registry_value(REG_FRAME_METHODS)?;
|
||||
@@ -225,6 +259,62 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> {
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// `RegisterUnitEvent(event, unit1 [, unit2])`: the stock registration, narrowed to the units
|
||||
// whose fire the frame wants. Everything about the dispatch stays the reference's — the frame
|
||||
// goes into the same `event_to_frames` list in the same registration order — and the filter is
|
||||
// read in `tick.rs`'s walk, which skips a frame whose row for this event does not name `arg1`.
|
||||
//
|
||||
// Upstream added it so a unit frame could stop waking for 39 other raid members' `UNIT_AURA`;
|
||||
// here the point is that addons written for it call it **unguarded**. `Cinematic/Frames.lua`
|
||||
// registers `UNIT_AURA` for `"player"` this way at file scope (`:2611`), so the nil method
|
||||
// stopped the file dead and took the rest of the addon's frame work with it.
|
||||
//
|
||||
// With no unit named at all this is a plain `RegisterEvent`: upstream's C function raises on a
|
||||
// missing `unit1`, and widening is the one outcome that cannot silently lose an event.
|
||||
m.set(
|
||||
"RegisterUnitEvent",
|
||||
lua.create_function(
|
||||
|lua, (this, event, u1, u2): (Table, String, Option<String>, Option<String>)| {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let units: Vec<String> = [u1, u2].into_iter().flatten().collect();
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
|
||||
let list = model.event_to_frames.entry(event.clone()).or_default();
|
||||
if !list.contains(&h) {
|
||||
list.push(h);
|
||||
}
|
||||
model
|
||||
.frame_events
|
||||
.entry(h)
|
||||
.or_default()
|
||||
.insert(event.clone());
|
||||
let rows = model.frame_unit_events.entry(h).or_default();
|
||||
if units.is_empty() {
|
||||
rows.remove(&event);
|
||||
} else {
|
||||
// A re-register replaces the unit list rather than adding to it, as upstream's
|
||||
// does: the call states the whole filter.
|
||||
rows.insert(event, units);
|
||||
}
|
||||
Ok(())
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
|
||||
// `SetFixedFrameStrata(on)` / `SetFixedFrameLevel(on)`: 9.0 added these so a frame could tell
|
||||
// the *frame manager* to stop re-deciding its strata and level when a parent moved. This
|
||||
// client has no such manager — strata and level are whatever `SetFrameStrata`/`SetFrameLevel`
|
||||
// last wrote, and nothing else ever rewrites them — so the guarantee the caller is asking for
|
||||
// already holds, and a no-op is the honest answer rather than a stub standing in for absent
|
||||
// behaviour. Ace3 calls both unguarded right after building its dialog
|
||||
// (`AceConfigDialog-3.0.lua:575-576`), which is where OpenMapClassic stopped once the border
|
||||
// template existed.
|
||||
for name in ["SetFixedFrameStrata", "SetFixedFrameLevel"] {
|
||||
m.set(
|
||||
name,
|
||||
lua.create_function(|_, (_this, _on): (Table, Option<Value>)| Ok(()))?,
|
||||
)?;
|
||||
}
|
||||
|
||||
// `GetEffectiveAlpha()`: the propagated alpha the arena already resolves, where the stock
|
||||
// `GetAlpha` answers only this frame's own factor.
|
||||
m.set(
|
||||
@@ -420,7 +510,7 @@ pub(in crate::script) fn dispatch_attribute_click(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::script::UiScript;
|
||||
use crate::script::{ScriptValue, UiScript};
|
||||
|
||||
fn frame(s: &UiScript) {
|
||||
s.run(r#"f = CreateFrame("Frame", "CAFrame")"#).unwrap();
|
||||
@@ -778,6 +868,56 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// `RegisterUnitEvent` narrows the fire to the units it names and changes nothing else: the
|
||||
/// frame sits in the same list in registration order, a fire for another unit is simply not
|
||||
/// delivered, and a plain `RegisterEvent` afterwards widens it back — the shape upstream has,
|
||||
/// and the one `Cinematic/Frames.lua:2611` registers `UNIT_AURA` for `"player"` with.
|
||||
#[test]
|
||||
fn register_unit_event_hears_only_its_units_until_a_plain_register_widens_it() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.run(
|
||||
r#"
|
||||
seen = ""
|
||||
local f = CreateFrame("Frame", "UnitFiltered")
|
||||
f:RegisterUnitEvent("UNIT_AURA", "player", "pet")
|
||||
f:SetScript("OnEvent", function() seen = seen .. arg1 .. "," end)
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
s.eval::<u32>(r#"return UnitFiltered:IsEventRegistered("UNIT_AURA")"#)
|
||||
.unwrap(),
|
||||
1,
|
||||
"the registration itself is the stock one"
|
||||
);
|
||||
for unit in ["target", "player", "party1", "pet"] {
|
||||
s.fire_event("UNIT_AURA", vec![ScriptValue::Str(unit.into())]);
|
||||
}
|
||||
assert_eq!(
|
||||
s.eval::<String>("return seen").unwrap(),
|
||||
"player,pet,",
|
||||
"only the two named units were delivered"
|
||||
);
|
||||
|
||||
// A plain register widens it, and the frame keeps its place rather than re-queueing.
|
||||
s.run(r#"UnitFiltered:RegisterEvent("UNIT_AURA") seen = """#)
|
||||
.unwrap();
|
||||
s.fire_event("UNIT_AURA", vec![ScriptValue::Str("target".into())]);
|
||||
assert_eq!(s.eval::<String>("return seen").unwrap(), "target,");
|
||||
|
||||
// And unregistering drops the filter with the registration, so a later plain register is
|
||||
// not still narrowed by a stale row.
|
||||
s.run(
|
||||
r#"UnitFiltered:RegisterUnitEvent("UNIT_AURA", "player")
|
||||
UnitFiltered:UnregisterEvent("UNIT_AURA")
|
||||
UnitFiltered:RegisterEvent("UNIT_AURA")
|
||||
seen = """#,
|
||||
)
|
||||
.unwrap();
|
||||
s.fire_event("UNIT_AURA", vec![ScriptValue::Str("target".into())]);
|
||||
assert_eq!(s.eval::<String>("return seen").unwrap(), "target,");
|
||||
}
|
||||
|
||||
/// `SetShown` on a region is a composition, so it is only correct while the pair it composes
|
||||
/// still reaches the leaf tables. `REGION_LEAF_SHARED` registers them per-leaf rather than on
|
||||
/// the Region map, which is the kind of arrangement a tidying pass moves — pinned here so that
|
||||
|
||||
@@ -712,6 +712,14 @@ pub(crate) struct Model {
|
||||
/// dispatches in that order across frames. Re-registering keeps a frame's position.
|
||||
pub(crate) event_to_frames: HashMap<String, Vec<FrameHandle>>,
|
||||
pub(crate) frame_events: HashMap<FrameHandle, HashSet<String>>,
|
||||
/// ClassicAPI's `RegisterUnitEvent` (`compat/classic_api/frame.rs`): per frame, per event, the
|
||||
/// unit tokens whose fire this frame wants. A frame with a row for an event is dispatched only
|
||||
/// when `arg1` is one of them, which is the whole of what the modern verb does — the
|
||||
/// registration itself is the stock one, in the stock list, in registration order.
|
||||
///
|
||||
/// Empty for every frame that used plain `RegisterEvent`, and `fire_event_into` checks the map
|
||||
/// is non-empty before looking anything up, so an install with no such addon pays one load.
|
||||
pub(crate) frame_unit_events: HashMap<FrameHandle, HashMap<String, Vec<String>>>,
|
||||
/// ClassicAPI's `SetAttribute`/`GetAttribute` (`compat/classic_api/frame.rs`): a plain
|
||||
/// per-frame key/value store, which is all those two verbs fundamentally are. The *secure*
|
||||
/// half of the modern secure-template system (restricted execution, the state and attribute
|
||||
@@ -1986,6 +1994,7 @@ impl Model {
|
||||
model_facts_wanted: Vec::new(),
|
||||
dirty_editboxes: Vec::new(),
|
||||
event_to_frames: HashMap::new(),
|
||||
frame_unit_events: HashMap::new(),
|
||||
frame_events: HashMap::new(),
|
||||
frame_attributes: HashMap::new(),
|
||||
all_event_frames: Vec::new(),
|
||||
|
||||
@@ -25,6 +25,12 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
if !list.contains(&h) {
|
||||
list.push(h);
|
||||
}
|
||||
// A plain register after `RegisterUnitEvent` widens the registration back to every
|
||||
// unit, as it does upstream: the frame keeps its place in the list and loses the
|
||||
// filter.
|
||||
if let Some(filters) = model.frame_unit_events.get_mut(&h) {
|
||||
filters.remove(&event);
|
||||
}
|
||||
model.frame_events.entry(h).or_default().insert(event);
|
||||
Ok(())
|
||||
})?,
|
||||
@@ -40,6 +46,9 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
if let Some(set) = model.frame_events.get_mut(&h) {
|
||||
set.remove(&event);
|
||||
}
|
||||
if let Some(filters) = model.frame_unit_events.get_mut(&h) {
|
||||
filters.remove(&event);
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
@@ -63,6 +72,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
let h = frame_handle_of(lua, &this)?;
|
||||
let mut model = lua.app_data_mut::<Model>().expect("model");
|
||||
let events = model.frame_events.remove(&h).unwrap_or_default();
|
||||
model.frame_unit_events.remove(&h);
|
||||
for event in events {
|
||||
if let Some(list) = model.event_to_frames.get_mut(&event) {
|
||||
list.retain(|x| x != &h);
|
||||
|
||||
@@ -290,6 +290,44 @@ 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",
|
||||
),
|
||||
(
|
||||
"CanWordWrap",
|
||||
"FontString",
|
||||
"ClassicAPI: the read half of SetWordWrap, so the pair round-trips. True by default, \
|
||||
which is what this client's layout actually does",
|
||||
),
|
||||
(
|
||||
"SetFixedFrameLevel",
|
||||
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
|
||||
"ClassicAPI: 9.0 told the frame manager to stop re-deciding a frame's level. There is no \
|
||||
such manager here — level is whatever SetFrameLevel last wrote and nothing rewrites it — \
|
||||
so the guarantee already holds and the no-op is the honest answer. Ace3 calls it \
|
||||
unguarded at AceConfigDialog-3.0.lua:576",
|
||||
),
|
||||
(
|
||||
"SetFixedFrameStrata",
|
||||
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
|
||||
"ClassicAPI: as SetFixedFrameLevel, for strata. Ace3, AceConfigDialog-3.0.lua:575",
|
||||
),
|
||||
(
|
||||
"SetWordWrap",
|
||||
"FontString",
|
||||
"ClassicAPI: 3.x's whether-to-wrap switch, which 1.12 has no spelling for (its \
|
||||
SetNonSpaceWrap decides WHERE a wrap may fall, not whether one happens). Stored, not \
|
||||
applied, like SetSpacing and for the same reason — the layout wraps to the declared \
|
||||
width unconditionally — and kept because the missing setter cost Cinematic its whole \
|
||||
options panel at Options.lua:4130",
|
||||
),
|
||||
(
|
||||
"RegisterUnitEvent",
|
||||
"Button CheckButton ColorSelect DressUpModel EditBox Frame GameTooltip LootButton MessageFrame Minimap Model MovieFrame PlayerModel ScrollFrame ScrollingMessageFrame SimpleHTML Slider StatusBar TabardModel WorldFrame",
|
||||
"ClassicAPI: the stock registration narrowed to named units — same list, same order, and \
|
||||
tick.rs's walk skips a frame whose row for the event does not name arg1. Addons written \
|
||||
for it do not guard it: Cinematic/Frames.lua:2611 registers UNIT_AURA for \"player\" at \
|
||||
file scope, so the nil method stopped the file dead. On every frame kind rather than \
|
||||
Frame alone because event registration lives on the one shared frame method table, which \
|
||||
is where 1.12 keeps RegisterEvent too — the two are registered alike or neither is",
|
||||
),
|
||||
(
|
||||
"SetColorTexture",
|
||||
"Texture",
|
||||
|
||||
@@ -121,7 +121,23 @@ pub(crate) fn fire_event_into(lua: &Lua, event: &str, args: Vec<ScriptValue>) ->
|
||||
.get(event)
|
||||
.and_then(|l| l.get(pos + 1).copied());
|
||||
let id = model.frame_id(h);
|
||||
// ClassicAPI's `RegisterUnitEvent`: a frame holding a unit filter for this event hears
|
||||
// it only for those units (`arg1`). The map is empty on an install with no such addon,
|
||||
// and a frame that registered plainly has no row, so the common path is one
|
||||
// `is_empty` and nothing else.
|
||||
let filtered_out = !model.frame_unit_events.is_empty()
|
||||
&& model
|
||||
.frame_unit_events
|
||||
.get(&h)
|
||||
.and_then(|m| m.get(event))
|
||||
.is_some_and(|units| {
|
||||
!matches!(args.first(), Some(ScriptValue::Str(u)) if units.contains(u))
|
||||
});
|
||||
drop(model);
|
||||
if filtered_out {
|
||||
at = next;
|
||||
continue;
|
||||
}
|
||||
reach.reached += 1;
|
||||
if let Err(e) = event::fire_event_handler(lua, id, event, &args) {
|
||||
model_mut().record_script_error(e.to_string());
|
||||
|
||||
@@ -441,6 +441,15 @@ pub(crate) struct RegionData {
|
||||
/// `SetNonSpaceWrap`/`CanNonSpaceWrap` (`0x79e9f0`/`0x79ead0`), `None` on. Stored, not applied:
|
||||
/// the reference's flag `0x40` drives a mid-word wrap and the ellipsis fit count (`0x771ec0`).
|
||||
pub(crate) non_space_wrap: Option<bool>,
|
||||
/// `SetWordWrap`/`CanWordWrap` (`compat/classic_api/frame.rs`), `None` on — 3.x's switch for
|
||||
/// whether a FontString wraps at all, which 1.12 has no counterpart to (its own
|
||||
/// `SetNonSpaceWrap` above decides *where* a wrap may fall, not whether one happens).
|
||||
/// **Stored, not applied**, the same footing as [`Self::spacing`]: this client's text layout
|
||||
/// wraps to the declared width unconditionally and has no "one line, clipped" mode for the
|
||||
/// number to drive. Kept because the pair must round-trip and because the absence of the
|
||||
/// *setter* is what costs — `Cinematic/Options.lua:4130` calls it on a label and aborted the
|
||||
/// whole options build there.
|
||||
pub(crate) word_wrap: Option<bool>,
|
||||
/// `SetSpacing`/`GetSpacing`: extra pixels between the lines of a wrapped FontString.
|
||||
/// **Stored, not applied** — this client's text layout models no line gap at all, so there is
|
||||
/// nothing for the number to drive yet. Kept rather than discarded because the pair must
|
||||
|
||||
Reference in New Issue
Block a user