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" + ); +}