ui: a nameplate now carries the unit token its own event names

`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<N>` 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-11-02 14:51:37 -07:00
co-authored by Claude Opus 5
parent 607ce45788
commit c93a0be93f
2 changed files with 104 additions and 1 deletions
+50 -1
View File
@@ -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::<Model>()
.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
@@ -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::<String>(
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::<String>("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::<i64>("return table.getn(createdFrames)").unwrap();
drive(&mut s, &[]);
assert_eq!(
s.eval::<String>("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"
);
}