From c93a0be93ff9075393d6aed5b8122a37a5a67086 Mon Sep 17 00:00:00 2001 From: grom Date: Mon, 2 Nov 2026 14:51:37 -0700 Subject: [PATCH] ui: a nameplate now carries the unit token its own event names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `plate.namePlateUnitToken` was set on nothing. It is how every later-era plate addon gets from a frame back to its unit — `plate.namePlateUnitToken or (plate.UnitFrame and plate.UnitFrame.unit)`, six sites in `Cinematic/Effects.lua` and `PlateParts.lua` alone — so an addon walking `C_NamePlate.GetNamePlates()` had a frame's rect and nothing about its unit, and could not tell a friendly plate from a hostile one. The measured symptom was grom's friendly nameplates switching themselves on the moment CineMode started, on a profile whose every friendly-plate box is unticked. That CVar write is the addon's and it is deliberate: outside instances Cinematic switches every plate kind on so visibility is decided by *fading* instead, because fades can change in a fight and plate settings cannot. Having switched them all on, it then faded none of them, because it could not identify one. The token already existed — `script/nameplate.rs` has had the slot-bound `nameplate` tokens and the three plate events since the pool was built. All that was missing was publishing the slot's token on the frame: set before `NAME_PLATE_UNIT_ADDED` and cleared after `NAME_PLATE_UNIT_REMOVED`, so a handler for either event reads the field that event is about, and a pooled frame reused for the next unit cannot answer the departed one's token. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- crates/benilla-ui/src/script/nameplate.rs | 51 +++++++++++++++++- .../src/script/tests/nameplate_tokens.rs | 54 +++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/crates/benilla-ui/src/script/nameplate.rs b/crates/benilla-ui/src/script/nameplate.rs index a17c0051..0f9a0b18 100644 --- a/crates/benilla-ui/src/script/nameplate.rs +++ b/crates/benilla-ui/src/script/nameplate.rs @@ -382,14 +382,62 @@ impl UiScript { super::event::fire_global(lua, "NAME_PLATE_CREATED", &[ScriptValue::Frame(id)]); } for slot in effects.added { + self.set_plate_unit_token(slot, true); super::event::fire_global( - lua, + self.lua(), "NAME_PLATE_UNIT_ADDED", &[ScriptValue::Str(nameplate_token(slot))], ); } } + /// Put `namePlateUnitToken` on (or take it off) the plate frame holding `slot`'s GUID. + /// + /// Retail's nameplates are FrameXML's, and the field is how **every** later-era plate addon + /// gets from a frame back to its unit: `plate.namePlateUnitToken or plate.UnitFrame.unit` is + /// the idiom, six times in `Cinematic/Effects.lua` and `PlateParts.lua` alone. Without it an + /// addon that walks `GetNamePlates()` can read a plate's rect and nothing about its unit, so + /// it cannot tell a friendly plate from a hostile one — and Cinematic, which switches every + /// plate kind ON in CineMode precisely so it can decide visibility by *fading* instead, then + /// faded none of them. The measured symptom was friendly nameplates appearing the moment + /// CineMode started, on a profile whose every friendly-plate box is unticked. + /// + /// Set before `NAME_PLATE_UNIT_ADDED` and cleared after `NAME_PLATE_UNIT_REMOVED`, so a + /// handler for either event reads the field the event is about. The token string is the same + /// one the event carries, so `UnitIsFriend(plate.namePlateUnitToken, "player")` resolves + /// through the ordinary unit path. + fn set_plate_unit_token(&mut self, slot: usize, present: bool) { + let Some(frame) = ({ + let model = self.model_mut(); + model + .nameplates + .token_slots + .get(slot) + .copied() + .flatten() + .and_then(|guid| model.nameplates.frame_for_guid(guid)) + }) else { + return; + }; + let lua = self.lua(); + let id = lua + .app_data_mut::() + .expect("model app_data") + .frame_id(frame); + let Ok(table) = crate::script::object::frame_wrapper(lua, id) else { + return; + }; + let value = if present { + mlua::Value::String(match lua.create_string(nameplate_token(slot)) { + Ok(s) => s, + Err(_) => return, + }) + } else { + mlua::Value::Nil + }; + let _ = table.raw_set("namePlateUnitToken", value); + } + /// Fire `NAME_PLATE_UNIT_REMOVED` for each slot, then clear its token pair and free the slot. /// The firing comes first on purpose: see [`UiScript::sync_nameplates`] step 1. fn fire_plate_removals(&mut self, slots: &[usize]) { @@ -399,6 +447,7 @@ impl UiScript { "NAME_PLATE_UNIT_REMOVED", &[ScriptValue::Str(nameplate_token(slot))], ); + self.set_plate_unit_token(slot, false); self.set_unit(&nameplate_token(slot), None); self.set_unit(&nameplate_target_token(slot), None); // Freed only now, so the slot is reusable from the next claim on — and never inside diff --git a/crates/benilla-ui/src/script/tests/nameplate_tokens.rs b/crates/benilla-ui/src/script/tests/nameplate_tokens.rs index 8c2164af..59374c09 100644 --- a/crates/benilla-ui/src/script/tests/nameplate_tokens.rs +++ b/crates/benilla-ui/src/script/tests/nameplate_tokens.rs @@ -650,3 +650,57 @@ fn pfuis_own_oncreate_body_runs_on_the_frame_the_created_event_hands_it() { with a nil plate there, that cache would leak for the session" ); } + +/// `plate.namePlateUnitToken`: the field every later-era plate addon reads to get from a frame +/// back to its unit (`plate.namePlateUnitToken or plate.UnitFrame.unit`, six sites in Cinematic +/// alone). Live by the time `NAME_PLATE_UNIT_ADDED` runs and gone once `_REMOVED` has, so a +/// handler for either reads the field the event is about; and a plate walked out of +/// `GetNamePlates()` carries it too, which is the call an addon that fades plates actually makes. +#[test] +fn a_plate_carries_the_unit_token_its_event_names() { + let mut s = vm(); + drive(&mut s, &[feed(WOLF, "Wolf")]); + assert_eq!( + s.eval::( + r#"local p = C_NamePlate.GetNamePlates()[1] + return tostring(p.namePlateUnitToken) .. "/" .. tostring(UnitName(p.namePlateUnitToken))"# + ) + .unwrap(), + "nameplate1/Wolf", + "the walked plate names its own unit" + ); + + // The field is the event's own token while the added handler runs: recorded there, not after. + s.run( + r#"seen = nil + watch = CreateFrame("Frame") + watch:RegisterEvent("NAME_PLATE_UNIT_ADDED") + watch:RegisterEvent("NAME_PLATE_UNIT_REMOVED") + watch:SetScript("OnEvent", function() + local plate = C_NamePlate.GetNamePlateForUnit(arg1) + seen = event .. ":" .. tostring(plate and plate.namePlateUnitToken) + end)"#, + ) + .unwrap(); + drive(&mut s, &[feed(WOLF, "Wolf"), feed(BEAR, "Bear")]); + assert_eq!( + s.eval::("return tostring(seen)").unwrap(), + "NAME_PLATE_UNIT_ADDED:nameplate2", + "set before the added event, not after it" + ); + + // Everything leaves: the field goes with the unit, so a pooled frame reused for the next unit + // cannot answer the departed one's token. + let frames = s.eval::("return table.getn(createdFrames)").unwrap(); + drive(&mut s, &[]); + assert_eq!( + s.eval::("return tostring(createdFrames[1].namePlateUnitToken)") + .unwrap(), + "nil", + "cleared once the plate's unit is gone" + ); + assert_eq!( + frames, 2, + "two pooled frames, both of which outlive their units" + ); +}