nameplates are frames, guild names go overhead, and the camera pivots

- a nameplate is a real frame under WorldFrame, six regions and a health bar in the reference's order, so pfUI, ShaguTweaks and _Nameplates find and restyle plates instead of seeing none; the plate stays out of the mouse's way in this cut (2148)
- a player's guild shows under their overhead name in angle brackets, the line an NPC's subname already used, gated by the UnitNamePlayerGuild option at its stock on (2149)
- smart camera pivot is on, as 1.12 ships it: a mostly vertical drag with the camera pressed against something tilts the view instead of swinging the arm, easing home on release; in water the pivot rises for a swimmer (2149)
- Follow Terrain and head bob are built and ship off, as in 1.12: Follow Terrain leans the camera up or down the slope you are walking onto, and head bob sways the first-person eye in a figure of eight while you move (2149)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-11 20:55:15 +07:00
co-authored by Claude Fable 5.1
parent 7d100d8a36
commit 38e45badb9
20 changed files with 4166 additions and 548 deletions
+2
View File
@@ -110,6 +110,8 @@ mod measure;
mod merchant;
mod messageframe;
mod minimap;
pub mod nameplate;
pub use nameplate::{PlateGeometry, PlateState};
mod model;
mod modelframe;
mod net_stats;
+6
View File
@@ -1619,6 +1619,11 @@ pub(crate) struct Model {
/// The world-map seam ([`worldmap`](super::worldmap)): the pushed catalog/feed + the
/// engine-owned selection.
pub(crate) worldmap: super::worldmap::WorldMapState,
/// The V-key nameplate pool — engine-owned `Button` widgets under the `WorldFrame`, grown on
/// demand and never shrunk, in creation order (decision 2148). Lives here rather than on
/// [`super::UiScript`] because it IS model state: the plates are arena frames, and an addon
/// walking `WorldFrame:GetChildren()` reaches them like any other.
pub(crate) nameplates: super::nameplate::NamePlates,
/// The always-up world-state readout's rows ([`worldstate`](super::worldstate)), already
/// gated and resolved app-side.
pub(crate) worldstate: super::worldstate::WorldStateUiState,
@@ -2139,6 +2144,7 @@ impl Model {
quest_log_watched: Vec::new(),
server_unix_time: None,
worldmap: super::worldmap::WorldMapState::default(),
nameplates: super::nameplate::NamePlates::default(),
worldstate: super::worldstate::WorldStateUiState::default(),
pending_events: Vec::new(),
cursor_pos: (0.0, 0.0),
+668
View File
@@ -0,0 +1,668 @@
//! **The nameplate widgets** — `CGNamePlateFrame` as the frame-system object it actually is
//! (decision 2148; wow-re `system/ui/scratch/nameplate-lua-surface.md`, §5 eight-worker round +
//! orchestrator byte arbitration 2026-09-09, benilla dispatch).
//!
//! benilla drew V-plates for a year as quads pushed straight into the UI pass, which made them
//! invisible to Lua: a live run's `WorldFrame:GetChildren()` returned the two named FrameXML
//! frames and nothing else, and every 1.12 nameplate addon — all of which find plates by walking
//! exactly that list — found nothing to work with. The reference does the opposite, and this
//! module is that difference closed.
//!
//! ## What the reference builds, byte for byte
//!
//! The plate is a `CSimpleButton` subclass (ctor `0x7cb250`, `…\Ui\NamePlateFrame.cpp`, vtable
//! `0x81de50`, size `0x518`) whose **base constructor parents it to the `CGWorldFrame` singleton**
//! `ds:0xb4b2bc` (`0x7786a3` → `0x769318` → `SetParent 0x76ab10`, tail-appending at `0x76abdb`) —
//! so it is a real member of `[worldframe+0x300]`, returned by `GetChildren` and counted by
//! `GetNumChildren`, inheriting strata **0 (WORLD)** and taking level **1**.
//!
//! - **Identity:** `GetObjectType()` = `"Button"`, `IsObjectType` accepts Button/Frame/Region, and
//! `GetName()` is **nil** — `[this+0x98]` is zeroed at `0x76c50b` and only XML `name=` ever
//! writes it. Anonymity is load-bearing: `_Nameplates` and pfUI both reject a *named*
//! `WorldFrame` child.
//! - **Six regions, in creation order**: Border `0x7cb2c2`, Glow `0x7cb361`, Name `0x7cb3d7`,
//! Level `0x7cb479`, Skull `0x7cb512`, RaidIcon `0x7cb5b4`.
//! - **One child**, the health bar (`GetObjectType()` = `"StatusBar"`), whose own single region is
//! the BarFill texture `GetStatusBarTexture()` returns. **No cast bar exists in 1.12** — a second
//! child would be blanked by pfUI, which destructures `healthbar, castbar`.
//! - **Real anchors.** The ctor writes them through `0x767c70` — the *same* primitive and the same
//! `[layoutObj + 4 + 4·point]` storage the Lua `SetPoint` binding ends in — so `GetPoint`,
//! `GetLeft` and `GetWidth` answer truthfully. Name = `("BOTTOM", plate, "CENTER", 0, 0)`
//! (`0x7cb456`), Level = `("CENTER", plate, "BOTTOMRIGHT", …)` (`0x7cb7f5`); `_Nameplates`
//! *validates* both pairs and rejects a plate that fails them.
//! - **Levels, and this is the surprise:** the bar is put **below** the plate, not above it —
//! `0x7cb32e mov edx,[plate+0xc4]` (=1) · `0x7cb33c dec edx` · `0x7cb33e call 0x76a4f0(bar,0,0)`,
//! an absolute level that overrides what `SetParent` had just given it. Plate level 1, **bar
//! level 0**. Since the draw walk's layer loop is *outer* and its frame walk *inner* (`0x765920`:
//! layer counter `0x76593c`, frame walk `0x7659d0`–`0x7659f8`, `cmp eax,5` at `0x765a63`), the
//! whole bar frame drains before any plate layer — which is why the border's bevels cap the
//! fill's ends. The director pinned that look off a reference crop while `nameplate-vkey.md` §7
//! recorded the opposite order; the §5 round settled it in the director's favour and corrected
//! the note.
//! - **Layers** (`GetDrawLayer 0x79a6c0`, table `0x811a80`): Border ARTWORK, Glow **HIGHLIGHT**,
//! Name/Level/Skull OVERLAY (the ctor re-layers Name and Level off ARTWORK at `0x7cb438`/
//! `0x7cb4ea`), RaidIcon ARTWORK, BarFill ARTWORK on the bar. **No sub-level exists in 5875**;
//! within a layer it is creation order.
//! - **Scripts:** the engine installs none, and every C++ override tails into the base
//! unconditionally, so an addon's `SetScript` on OnShow/OnHide/OnUpdate/OnEnter/OnLeave/
//! OnMouseDown/OnMouseUp/OnClick fires. `RegisterForClicks(0x500)` = Left/Right on mouse-**up**.
//! - **Pooling:** destroy (`0x608a10`) never unparents — it hides and pushes onto a free list, and
//! reuse re-runs neither the ctor nor `SetParent`. So **the child-list index is stable for the
//! session**, and a retired plate is still returned *and counted* (the list walk has no
//! visibility filter). Every corpus addon's `registry[plate]` identity map rests on that.
//!
//! ## The consumer contract this has to keep
//!
//! Four independent 1.12 addons (`_Nameplates`, `CustomNameplates`, pfUI's vanilla branch,
//! ShaguTweaks' vanilla branch — all `## Interface: 11200`) destructure the two tuples
//! *positionally* and identify a plate by `regions[1]:GetTexture()` being exactly
//! `Interface\Tooltips\Nameplate-Border`. The byte-derived order and the corpus's agree element for
//! element — an independent empirical control on a byte derivation. So the composition is not ours
//! to choose; it is an ABI, and the tests transcribe each addon's own walk.
//!
//! **An addon takes the plate over**, which is the half a painter never had to respect: pfUI blanks
//! all six regions and paints its own, sets `SetAlpha(.95)` on non-targets itself, and ShaguTweaks
//! reparents the raid icon. So [`Plate::create`] writes the static properties (paths, fonts,
//! layers) exactly **once**, [`Plate::lay_out`] rewrites the window-derived anchors only when the
//! window moves, and [`Plate::drive`] writes only what actually changed this frame. The one
//! deliberate exception is the plate's own anchor, which the reference itself re-issues every dirty
//! frame from the seat `0x509ec0` (`ClearAllPoints` + `SetPoint`), wiping an addon anchor *on the
//! plate* there too — which is why every corpus addon anchors its overlay **to** the plate and
//! re-anchors the **bar**, never the plate.
use crate::layout::{Anchor, Point};
use crate::order::DrawLayer;
use crate::widget::{FrameHandle, FrameKind, KindState, RegionHandle, RegionKind};
use super::model::Model;
use super::UiScript;
use super::{FontShadow, JustifyV, TexCoords};
/// Region 1's art — and the identification test every corpus addon runs, which is why it is a
/// constant here rather than a caller's argument.
pub const BORDER_TEXTURE: &str = "Interface\\Tooltips\\Nameplate-Border";
/// Region 2's. `glow:IsShown()` is how pfUI reads *mouseover* (`nameplates.lua:601`).
pub const GLOW_TEXTURE: &str = "Interface\\Tooltips\\Nameplate-Glow";
/// Region 5's — the boss / out-of-range skull (`levelicon` to pfUI, `Boss` to CustomNameplates).
pub const SKULL_TEXTURE: &str = "Interface\\TargetingFrame\\UI-TargetingFrame-Skull";
/// Region 6's 4-column atlas.
pub const RAID_ICON_TEXTURE: &str = "Interface\\TargetingFrame\\UI-RaidTargetingIcons";
/// The health bar's fill — what `healthbar:GetStatusBarTexture()` hands back.
pub const BAR_FILL_TEXTURE: &str = "Interface\\TargetingFrame\\UI-TargetingFrame-BarFill";
/// `NAMEPLATE_FONT` — Friz Quadrata, the plate's own face.
pub const PLATE_FONT: &str = "Fonts\\FRIZQT__.TTF";
/// The plate's frame level, and the bar's — `plate.level − 1`, absolute and clamped ≥ 0
/// (`0x7cb32e`/`0x7cb33c`/`0x7cb33e` → `0x76a4f0`). The pair is what puts the border over the fill.
const PLATE_LEVEL: u16 = 1;
const BAR_LEVEL: u16 = 0;
/// Everything about a plate's geometry that follows from the *window* rather than from the unit,
/// in FrameXML units (the driver has already divided out the seam scale).
///
/// Applied to every pooled plate at once, and only when it changes — these are exactly the writes
/// that must not happen per frame, because an addon that re-anchors the bar would otherwise have
/// its anchor stamped over at 60 Hz.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PlateGeometry {
/// The plate frame's own size.
pub width: f32,
pub height: f32,
/// The health bar, from the plate's BOTTOMLEFT.
pub bar_off_x: f32,
pub bar_off_y: f32,
pub bar_width: f32,
pub bar_height: f32,
/// The level text / skull seat, from the plate's BOTTOMRIGHT (x is subtracted, y added).
pub level_off_x: f32,
pub level_off_y: f32,
/// The skull's square side.
pub skull_size: f32,
/// The raid icon's square side; it hangs off the plate's LEFT edge, vertically centred.
pub raid_size: f32,
/// Font heights for the name and the level.
pub name_height: f32,
pub level_height: f32,
}
/// One plate's per-frame state, as the app hands it over: everything that can move while a plate is
/// alive. Anything that cannot is written once, at creation.
#[derive(Clone, Debug, PartialEq)]
pub struct PlateState {
/// The unit this plate belongs to — its GUID, and **the plate's identity**.
///
/// The reference binds a plate to its unit for as long as the plate lives (`[unit+0xe60]`
/// holds the pointer; the free list hands out a plate at create and takes it back at destroy),
/// and every corpus addon depends on that: pfUI caches the name, level, colour and its own
/// overlay ON the frame (`registry[plate]`, `nameplate.original.*`) and only refreshes what
/// its `OnDataChanged` says moved. Handing widget *i* to whichever unit happens to sort i-th
/// this frame would make every plate's cached state wrong on the frame two units swap places —
/// which is why the pool is keyed by this rather than indexed by the driver's sort order.
pub key: u64,
/// Where the plate's **TOP-CENTRE** lands, in FrameXML units from the screen's bottom-left —
/// the reference's own seat (`nameplate-vkey.md` §8.5: the plate hangs below the point).
pub top_centre: (f32, f32),
/// `UNIT_FIELD_HEALTH` and `UNIT_FIELD_MAXHEALTH`, **raw**: `healthbar:GetValue()` is
/// `[bar+0x320]` (`0x78f5d0`), not a fraction, and `GetMinMaxValues()` answers `(0, max)`.
/// pfUI and CustomNameplates both divide, so a fraction here would read as 1 hp out of 1.
pub health: f32,
pub max_health: f32,
/// The bar's reaction colour — the four confirmed dwords. Addons reverse-engineer the unit's
/// faction from `GetStatusBarColor`, so these have to be exact.
pub bar_colour: [f32; 3],
/// The unit's name.
pub name: String,
/// The level text, or `None` to show the skull in the level's seat instead.
pub level: Option<u32>,
/// The level text's con colour.
pub level_colour: [f32; 3],
/// The raid-target mark, 0-based (`col = idx & 3`, `row = idx >> 2`), or `None`.
pub raid_icon: Option<u8>,
/// The plate's own alpha: **exactly 1.0 for the target**, dimmed otherwise. `GetAlpha() == 1`
/// is how pfUI and CustomNameplates detect the target plate — a contract, not a look.
pub alpha: f32,
/// Mouseover ∪ target: the bar's brighten.
pub lit: bool,
/// The plate rect is hovered right now — the name goes yellow (`0x7cb850` OnEnter, name-only)
/// and the glow region is shown, which is the mouseover signal addons read.
pub hovered: bool,
}
/// One pooled plate's widgets, in the reference's creation order.
#[derive(Debug)]
struct Plate {
frame: FrameHandle,
border: RegionHandle,
glow: RegionHandle,
name: RegionHandle,
level: RegionHandle,
skull: RegionHandle,
raid: RegionHandle,
bar: FrameHandle,
fill: RegionHandle,
/// Last frame's state, so [`Plate::drive`] writes only what moved. `None` = retired.
last: Option<PlateState>,
}
/// The plate pool — grown on demand, **never shrunk**, in creation order.
///
/// The reference's pool is unbounded and its free list FIFO; what matters to an addon is only that
/// an index, once handed out, keeps meaning the same Lua object, and that a retired plate stays in
/// the child list (hidden). pfUI and ShaguTweaks scan `initialized + 1 .. GetNumChildren()` and
/// never reset `initialized`, so a client that reordered or compacted this list would make them
/// miss every plate created afterwards — permanently, and silently.
#[derive(Debug, Default)]
pub(crate) struct NamePlates {
plates: Vec<Plate>,
/// Which pool slot each live unit holds, by [`PlateState::key`] — the reference's
/// `[unit+0xe60]`, from this side. A slot leaves this map only when its unit's plate is
/// retired, and the slot itself never moves.
assigned: std::collections::HashMap<u64, usize>,
/// The geometry every live plate is currently laid out under.
geometry: Option<PlateGeometry>,
}
/// What a sync produced that has to be fired **outside** the model borrow: the frames whose
/// effective visibility flipped (their `OnShow`/`OnHide`), and the bars whose value moved (their
/// `OnValueChanged`, which pfUI hooks — `nameplates.lua:393` — and reads `this:GetParent()` in).
#[derive(Default)]
struct SyncEffects {
visibility: Vec<FrameHandle>,
values: Vec<(FrameHandle, f32)>,
}
impl UiScript {
/// Drive the plate pool from the app's per-frame verdict: `states[i]` is the i-th **shown**
/// plate, and every pooled plate past `states.len()` is retired (hidden, never destroyed).
///
/// `geometry` is the window-derived half; passing an unchanged value costs nothing.
pub fn sync_nameplates(&mut self, geometry: PlateGeometry, states: &[PlateState]) {
let lua = self.lua();
let effects = {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
// No WorldFrame, no plates: a VM with no interface loaded (a bare test app, the addon
// harness) has nowhere to parent them, and inventing a root would put plates somewhere
// an addon's walk could never find them anyway.
let Some(world) = model.arena.lookup("WorldFrame") else {
return;
};
sync(&mut model, world, geometry, states)
};
super::event::fire_visibility_changes(lua, effects.visibility);
for (bar, value) in effects.values {
super::statusbar::fire_engine_value_changed(lua, bar, value);
}
}
/// How many plate widgets the pool holds — the plate driver's census, and the tests'.
pub fn nameplate_pool_len(&self) -> usize {
let lua = self.lua();
let model = lua.app_data_ref::<Model>().expect("model app_data");
model.nameplates.plates.len()
}
}
/// The whole per-frame drive: grow, re-lay-out if the window moved, write what changed, retire the
/// tail.
fn sync(
model: &mut Model,
world: FrameHandle,
geometry: PlateGeometry,
states: &[PlateState],
) -> SyncEffects {
let mut effects = SyncEffects::default();
let relayout = model.nameplates.geometry != Some(geometry);
model.nameplates.geometry = Some(geometry);
// Retire first, so a unit that left this frame frees its slot for one that arrived — the
// reference's destroy-then-create through the same free list, in the same tick.
let gone: Vec<u64> = model
.nameplates
.assigned
.keys()
.copied()
.filter(|k| !states.iter().any(|s| s.key == *k))
.collect();
for key in gone {
if let Some(i) = model.nameplates.assigned.remove(&key) {
Plate::retire(model, i, &mut effects);
}
}
for state in states {
let slot = match model.nameplates.assigned.get(&state.key) {
Some(&i) => i,
None => {
// The lowest free slot, else a new one at the tail. Lowest-free rather than the
// reference's FIFO free list: both keep every existing index meaning what it did,
// which is the only property an addon can observe.
let free = (0..model.nameplates.plates.len())
.find(|i| model.nameplates.plates[*i].last.is_none());
let i = free.unwrap_or_else(|| {
let plate = Plate::create(model, world, &mut effects);
model.nameplates.plates.push(plate);
model.nameplates.plates.len() - 1
});
model.nameplates.assigned.insert(state.key, i);
i
}
};
if relayout {
Plate::lay_out(model, slot, geometry);
}
Plate::drive(model, slot, world, state, &mut effects);
}
// A geometry change reaches the retired plates too: they are still in the child list, an addon
// can still read them, and they must not come back next frame laid out for the old window.
if relayout {
for i in 0..model.nameplates.plates.len() {
if model.nameplates.plates[i].last.is_none() {
Plate::lay_out(model, i, geometry);
}
}
}
effects
}
impl Plate {
/// Build one plate: the Button under the WorldFrame, its six regions in the reference's
/// creation order, and the health-bar child with its fill.
///
/// Everything here is written **once**. An addon that blanks a region (pfUI's `DisableObject`
/// does `SetTexture("")`) must find it still blank on the next frame.
fn create(model: &mut Model, world: FrameHandle, effects: &mut SyncEffects) -> Plate {
// Anonymous, and that is a requirement rather than an omission (module doc).
let frame = model.arena.create(FrameKind::Button, None, Some(world));
let strata = model
.arena
.frame(world)
.map(|f| f.strata)
.unwrap_or_default();
model.arena.set_frame_strata(frame, strata);
model.arena.set_frame_level(frame, PLATE_LEVEL, true);
// **The mouse bit stays OFF, and that is a deliberate gap, not an omission** (2148 §8).
// The reference's plate is mouse-enabled — hovering it publishes the mouseover
// (`0x7cb850` → `0x492890`) and a click selects through the button's own click slot. On
// this engine, a mouse-enabled frame under the cursor is what makes `PointerOverUi` true,
// and `target::hover::update_hover` returns early on exactly that: setting the bit without
// an engine-side hover/click seam takes the hover and gives nothing back — no model
// brighten, no lit bar, and no selection, because the click stops at the UI too. The seam
// comes first; then the bit, in the same change.
// `a_plate_does_not_yet_claim_the_ui_pointer` is the tripwire.
//
// It has to be turned OFF explicitly: a `Button` is born mouse-enabled here, faithfully —
// `CSimpleButton`'s ctor writes `[+0xcc] = 0x4` (`0x7786a3`), which is why the plate needs
// no `EnableMouse` of its own in the reference.
model.arena.set_mouse_enabled(frame, false);
let border = texture(model, frame, DrawLayer::Artwork, BORDER_TEXTURE);
let glow = texture(model, frame, DrawLayer::Highlight, GLOW_TEXTURE);
let name = font_string(model, frame, JustifyV::Bottom);
let level = font_string(model, frame, JustifyV::Middle);
let skull = texture(model, frame, DrawLayer::Overlay, SKULL_TEXTURE);
let raid = texture(model, frame, DrawLayer::Artwork, RAID_ICON_TEXTURE);
// ↑ THE ORDER. Six regions, and this sequence is the ABI (module doc).
// The one child, below the plate's own level so the border draws over the fill.
let bar = model.arena.create(FrameKind::StatusBar, None, Some(frame));
model.arena.set_frame_strata(bar, strata);
model.arena.set_frame_level(bar, BAR_LEVEL, true);
let fill = model
.arena
.create_region(bar, RegionKind::Texture, DrawLayer::Artwork, 0)
.expect("live bar");
model.region_data.entry(fill).or_default().texture = Some(BAR_FILL_TEXTURE.to_string());
if let Some(KindState::StatusBar(sb)) =
model.arena.frame_mut(bar).map(|f| &mut f.kind_state)
{
sb.bar = Some(fill);
sb.min = 0.0;
sb.max = 1.0;
sb.value = 0.0;
}
// The skull and the raid icon are the two regions that only sometimes show; the glow is
// shown on hover alone. The rest are always up while the plate is.
for rh in [glow, skull, raid] {
model.region_data.entry(rh).or_default().hidden = true;
}
// Born retired: a fresh plate belongs to no unit yet, and `IsShown()` is what an addon
// reads to tell a live plate from a pooled one.
effects
.visibility
.extend(model.arena.set_shown(frame, false));
Plate {
frame,
border,
glow,
name,
level,
skull,
raid,
bar,
fill,
last: None,
}
}
/// (Re)lay the static anchors — everything that follows from the window's size rather than the
/// unit. Called on the first drive and on a geometry change, never per frame.
fn lay_out(model: &mut Model, i: usize, g: PlateGeometry) {
let p = &model.nameplates.plates[i];
let (frame, bar) = (p.frame, p.bar);
let (border, glow, name, level, skull, raid) =
(p.border, p.glow, p.name, p.level, p.skull, p.raid);
let plate_id = model.frame_id(frame);
// The plate's own size; its position is per-frame and lives in `drive`.
let input = model.layout_inputs.entry(frame).or_default();
input.width = g.width;
input.height = g.height;
// The border fills the plate, and so does the glow — a rim over the whole frame.
for rh in [border, glow] {
model.region_data.entry(rh).or_default().anchors = vec![
Anchor::new(Point::TopLeft, plate_id, Point::TopLeft, 0.0, 0.0),
Anchor::new(Point::BottomRight, plate_id, Point::BottomRight, 0.0, 0.0),
];
}
// The health bar: BOTTOMLEFT + (off_x, off_y), explicit size.
let bar_input = model.layout_inputs.entry(bar).or_default();
bar_input.anchors = vec![Anchor::new(
Point::BottomLeft,
plate_id,
Point::BottomLeft,
g.bar_off_x,
g.bar_off_y,
)];
bar_input.width = g.bar_width;
bar_input.height = g.bar_height;
// The name — ("BOTTOM", plate, "CENTER", 0, 0), the pair `_Nameplates` validates.
let name_data = model.region_data.entry(name).or_default();
name_data.anchors = vec![Anchor::new(
Point::Bottom,
plate_id,
Point::Center,
0.0,
0.0,
)];
name_data.font_height = Some(g.name_height);
// The level — ("CENTER", plate, "BOTTOMRIGHT", −x, +y), the other validated pair.
let level_data = model.region_data.entry(level).or_default();
level_data.anchors = vec![Anchor::new(
Point::Center,
plate_id,
Point::BottomRight,
-g.level_off_x,
g.level_off_y,
)];
level_data.font_height = Some(g.level_height);
level_data.font_explicit.height = true;
model.touch_measure(level);
// The skull replaces the level number, in the level's own seat.
let skull_data = model.region_data.entry(skull).or_default();
skull_data.anchors = vec![Anchor::new(
Point::Center,
plate_id,
Point::BottomRight,
-g.level_off_x,
g.level_off_y,
)];
skull_data.size = Some((g.skull_size, g.skull_size));
// The raid icon hangs off the LEFT edge, vertically centred.
let raid_data = model.region_data.entry(raid).or_default();
raid_data.anchors = vec![Anchor::new(Point::Right, plate_id, Point::Left, 0.0, 0.0)];
raid_data.size = Some((g.raid_size, g.raid_size));
// Anchor TARGETS moved (from nothing to the plate) on the first lay-out, so this is the
// conservative touch — the only one in this module, and it runs once per geometry change.
model.touch_layout();
}
/// The per-frame write: position, health, texts, colours, shown flags, alpha — **and nothing
/// else**, each only when it moved.
fn drive(
model: &mut Model,
i: usize,
world: FrameHandle,
state: &PlateState,
effects: &mut SyncEffects,
) {
let p = &model.nameplates.plates[i];
let (frame, bar) = (p.frame, p.bar);
let (glow, name, level, skull, raid, fill) =
(p.glow, p.name, p.level, p.skull, p.raid, p.fill);
let last = p.last.clone();
let world_id = model.frame_id(world);
// The seat, re-issued whenever it moves, exactly like the reference's own (`0x509ec0`:
// ClearAllPoints then SetPoint) — which is why an addon anchor on the plate does not
// survive there either. A moved OFFSET keeps the graph's edges, so the precise touch
// applies; the first seat adds an edge and takes the retarget.
if last
.as_ref()
.is_none_or(|l| l.top_centre != state.top_centre)
{
let fresh = model
.layout_inputs
.get(&frame)
.is_none_or(|i| i.anchors.is_empty());
let input = model.layout_inputs.entry(frame).or_default();
input.anchors = vec![Anchor::new(
Point::Top,
world_id,
Point::BottomLeft,
state.top_centre.0,
state.top_centre.1,
)];
if fresh {
model.touch_layout_retarget_frame(frame, &[], &[world_id]);
} else {
model.touch_layout_frame(frame);
}
}
if last.is_none() {
effects
.visibility
.extend(model.arena.set_shown(frame, true));
}
if last.as_ref().is_none_or(|l| l.alpha != state.alpha) {
model.arena.set_alpha(frame, state.alpha);
}
// The bar reports RAW health (module doc). Its fill carries the reaction colour, lifted by
// a uniform brighten while the unit is the mouseover or the target — 0184's form of the
// highlight, in place of the additive `Nameplate-Glow` rim.
if last
.as_ref()
.is_none_or(|l| l.health != state.health || l.max_health != state.max_health)
{
if let Some(KindState::StatusBar(sb)) =
model.arena.frame_mut(bar).map(|f| &mut f.kind_state)
{
sb.min = 0.0;
sb.max = state.max_health;
sb.value = state.health.clamp(0.0, state.max_health);
}
effects.values.push((bar, state.health));
}
if last
.as_ref()
.is_none_or(|l| l.bar_colour != state.bar_colour || l.lit != state.lit)
{
let boost = if state.lit { LIT_BOOST } else { 1.0 };
let c = state.bar_colour;
model.region_data.entry(fill).or_default().vertex_color =
Some([c[0] * boost, c[1] * boost, c[2] * boost, 1.0]);
}
// The name: white, yellow while the plate itself is hovered.
if last.as_ref().is_none_or(|l| l.name != state.name) {
model.region_data.entry(name).or_default().text = Some(state.name.clone());
model.touch_measure(name);
model.touch_layout_region(name);
}
if last.as_ref().is_none_or(|l| l.hovered != state.hovered) {
let c = if state.hovered {
[1.0, 1.0, 0.0, 1.0]
} else {
[1.0, 1.0, 1.0, 1.0]
};
model.region_data.entry(name).or_default().vertex_color = Some(c);
// `glow:IsShown()` IS the mouseover signal (pfUI `nameplates.lua:601`). The region is
// shown for real; that it paints nothing is 0184's deviation, not a missing region.
model.region_data.entry(glow).or_default().hidden = !state.hovered;
}
// Level text and skull are mutually exclusive, in one seat.
if last
.as_ref()
.is_none_or(|l| l.level != state.level || l.level_colour != state.level_colour)
{
match state.level {
Some(n) => {
let c = state.level_colour;
let data = model.region_data.entry(level).or_default();
data.text = Some(n.to_string());
data.vertex_color = Some([c[0], c[1], c[2], 1.0]);
data.hidden = false;
model.region_data.entry(skull).or_default().hidden = true;
model.touch_measure(level);
model.touch_layout_region(level);
}
None => {
model.region_data.entry(level).or_default().hidden = true;
model.region_data.entry(skull).or_default().hidden = false;
}
}
}
// The raid mark: the 4-column atlas cell, shown only while the unit carries one.
if last.as_ref().is_none_or(|l| l.raid_icon != state.raid_icon) {
match state.raid_icon {
Some(idx) => {
let (u0, v0) = (f32::from(idx & 3) * 0.25, f32::from(idx >> 2) * 0.25);
let data = model.region_data.entry(raid).or_default();
data.tex_coords = Some(TexCoords::Rect([u0, u0 + 0.25, v0, v0 + 0.25]));
data.hidden = false;
}
None => model.region_data.entry(raid).or_default().hidden = true,
}
}
model.nameplates.plates[i].last = Some(state.clone());
}
/// Retire a plate: **hide it, never destroy it** (`0x608a10` — unlink from the active list,
/// push onto the free list, `Hide`). It keeps its slot in `WorldFrame`'s child list, keeps its
/// index, and comes back as the same Lua object, which is the contract every addon's
/// `registry[plate]` identity map rests on.
fn retire(model: &mut Model, i: usize, effects: &mut SyncEffects) {
if model.nameplates.plates[i].last.is_none() {
return;
}
let frame = model.nameplates.plates[i].frame;
effects
.visibility
.extend(model.arena.set_shown(frame, false));
model.nameplates.plates[i].last = None;
}
}
/// One of the plate's texture regions, with its path — created in the ABI's order by the caller.
fn texture(model: &mut Model, frame: FrameHandle, layer: DrawLayer, path: &str) -> RegionHandle {
let rh = model
.arena
.create_region(frame, RegionKind::Texture, layer, 0)
.expect("live plate");
model.region_data.entry(rh).or_default().texture = Some(path.to_string());
rh
}
/// One of the plate's two FontStrings. Both are OVERLAY — the ctor re-layers them off ARTWORK
/// (`0x7cb438`, `0x7cb4ea`) — and both take `NAMEPLATE_FONT` with the plate's black 1 px drop
/// shadow; the heights arrive with the geometry.
///
/// `justify_v` is the one benilla-side compensation here, and it is the old painter's, kept: the
/// plate's name is seated by its **ink**, not by the line box. Our shaper's box is ascent-heavy
/// for Friz, so a box-seated `BOTTOM` anchor drops the name ~8 px — half of it over the plate —
/// which is the director's old "the level is not aligned" in its other half. `Bottom` puts the
/// text where the reference's own metrics put it.
fn font_string(model: &mut Model, frame: FrameHandle, justify_v: JustifyV) -> RegionHandle {
let rh = model
.arena
.create_region(frame, RegionKind::FontString, DrawLayer::Overlay, 0)
.expect("live plate");
let data = model.region_data.entry(rh).or_default();
data.font_path = Some(PLATE_FONT.to_string());
data.font_explicit.face = true;
// The plate text's shadow (`nameplate-vkey.md` §7: black, ±0.001 gx, director-tuned to a
// constant 1 px — the recorded value rounds to 2 px at a large window and reads chunky).
data.font_shadow = Some(FontShadow {
offset: [1.0, -1.0],
color: [0.0, 0.0, 0.0, 1.0],
});
data.justify.set_v(justify_v);
rh
}
/// The LIT (mouseover ∪ target) bar brighten — a uniform multiplicative lift of the fill tint,
/// gradient untouched (0184: the recorded additive `Nameplate-Glow` rim read as hard edge lines on
/// our linear-blending pipeline, and the director pinned the brighten instead).
///
/// Quad colours are client-space sRGB (`srgb_quad_color` linearizes them), so this multiplies
/// ENCODED texels ~1:1 — the gamma-space modulate the client's FFP would do. **255/215 is the
/// largest clean value**: the fill texture's encoded peak (215) lands exactly on white, no row
/// clips, and every row scales by the same visible factor. (A first cut of 1.45 assumed a
/// linear-space multiply and flattened six rows against white — the very gradient change the
/// director banned.)
const LIT_BOOST: f32 = 255.0 / 215.0;
+26
View File
@@ -264,6 +264,32 @@ fn fire_value_changed(lua: &Lua, this: &Table, changed: Option<f32>) -> mlua::Re
Ok(())
}
/// `OnValueChanged` for a bar the ENGINE moved rather than Lua — the nameplate health bars, whose
/// value the plate driver re-sets from the unit's descriptor each time it changes (decision 2148).
///
/// The reference fires the same script from the same place: the plate's bar is driven by a
/// GUID-watch callback (`0x7cc570`, registered `0x467e70`) that re-reads health and re-sets the bar
/// synchronously on every server update, and `SetValue` is `SetValue`. pfUI hooks this
/// (`nameplates.lua:393` `HookScript(healthbar, "OnValueChanged", …)`) and reads `this:GetParent()`
/// inside the handler, so a driver that moved the value silently would leave it blind.
pub(super) fn fire_engine_value_changed(lua: &Lua, bar: crate::widget::FrameHandle, value: f32) {
let id = {
let mut model = lua.app_data_mut::<Model>().expect("model app_data");
model.frame_id(bar)
};
if let Err(e) = event::fire_widget_handler(
lua,
id,
"OnValueChanged",
vec![Value::Number(f64::from(value))],
) {
lua.app_data_mut::<Model>()
.expect("model app_data")
.errors
.push(e.to_string());
}
}
/// A Lua number-ish → f32 (nil/other → 0.0), for the color-form arguments.
fn num_f32(v: &Value) -> f32 {
match v {
@@ -28,6 +28,7 @@ mod model_clock;
mod modelframe;
mod movable;
mod name_targets;
mod nameplate;
mod object_model;
mod reference_surface;
mod region_map;
@@ -0,0 +1,489 @@
//! The nameplate ABI, as the four vanilla addons actually read it (decision 2148).
//!
//! These are not "does the pool work" tests. Each is a transcription of a real corpus idiom — the
//! same walk, the same destructuring, the same identification test — so a change that would break
//! a shipped addon fails **here** rather than on the director's screen.
//!
//! Sources, all `## Interface: 11200`: `pfUI/modules/nameplates.lua` (+ `compat/vanilla.lua`),
//! `ShaguTweaks/libs/libnameplate.lua`, `CustomNameplates/CustomNameplates.lua`,
//! `_Nameplates/_Nameplates.lua`. The byte side is wow-re
//! `system/ui/scratch/nameplate-lua-surface.md` (§5, 2026-09-09).
use crate::script::{PlateGeometry, PlateState, UiScript};
/// A VM with a `WorldFrame` to hang plates off — the only thing [`UiScript::sync_nameplates`]
/// requires of a session.
fn vm() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
s.run(r#"WorldFrame = CreateFrame("WorldFrame", "WorldFrame") WorldFrame:SetAllPoints()"#)
.unwrap();
s.resolve();
s
}
/// The plate size at the capture window (`vplates` scenario: one gx unit = 1280 px, so the
/// 0.1 × 0.025 plate is 128 × 32 px, and at seam scale 1.0 that is 128 × 32 units).
fn geometry() -> PlateGeometry {
PlateGeometry {
width: 128.0,
height: 32.0,
bar_off_x: 4.0,
bar_off_y: 4.0,
bar_width: 103.0,
bar_height: 9.0,
level_off_x: 11.8,
level_off_y: 9.1,
skull_size: 12.8,
raid_size: 25.6,
name_height: 12.8,
level_height: 11.0,
}
}
fn plate(name: &str, health: f32, max: f32) -> PlateState {
PlateState {
// The unit's GUID stands in as a hash of its name: what matters to these tests is that the
// same unit keeps the same key across frames and two units never share one.
key: name.bytes().map(u64::from).sum(),
top_centre: (500.0, 400.0),
health,
max_health: max,
bar_colour: [1.0, 0.0, 0.0],
name: name.to_string(),
level: Some(12),
level_colour: [1.0, 1.0, 0.0],
raid_icon: None,
alpha: 1.0,
lit: false,
hovered: false,
}
}
/// Drive N plates and settle the layout, the way one app frame does.
fn drive(s: &mut UiScript, states: &[PlateState]) {
s.sync_nameplates(geometry(), states);
s.resolve();
}
/// pfUI + ShaguTweaks share one lineage: `GetNumChildren` grew, so walk the tail.
///
/// `initialized` is never reset in either addon, so a client whose child list SHRANK or REORDERED
/// would make them miss every plate created after the old high-water mark — permanently, and with
/// no error. This is the test that a retired plate keeps its slot.
#[test]
fn the_worldframe_child_list_only_grows_and_keeps_its_order() {
let mut s = vm();
assert_eq!(
s.eval::<i64>("return WorldFrame:GetNumChildren()").unwrap(),
0
);
drive(
&mut s,
&[plate("Wolf", 30.0, 40.0), plate("Bear", 10.0, 90.0)],
);
assert_eq!(
s.eval::<i64>("return WorldFrame:GetNumChildren()").unwrap(),
2
);
// One unit leaves: the plate is HIDDEN, and the child list does not shrink.
drive(&mut s, &[plate("Wolf", 30.0, 40.0)]);
assert_eq!(
s.eval::<i64>("return WorldFrame:GetNumChildren()").unwrap(),
2
);
assert!(s
.eval::<bool>(
"local a, b = WorldFrame:GetChildren() return a:IsShown() == 1 and b:IsShown() == nil"
)
.unwrap());
// A third unit arrives: it takes the pooled plate back — the count goes to 2, not 3.
drive(
&mut s,
&[plate("Wolf", 30.0, 40.0), plate("Boar", 5.0, 50.0)],
);
assert_eq!(
s.eval::<i64>("return WorldFrame:GetNumChildren()").unwrap(),
2
);
assert_eq!(s.nameplate_pool_len(), 2);
}
/// pfUI's `IsNamePlate` (`nameplates.lua:70`) and ShaguTweaks' (`libnameplate.lua:12`), verbatim in
/// shape: the object type is `"Button"`, and region #1 is a `Texture` whose `GetTexture()` is
/// exactly `Interface\Tooltips\Nameplate-Border`.
///
/// `CustomNameplates` runs the texture half against **every** WorldFrame child
/// (`CustomNameplates.lua:93`), so `GetRegions()` must also be safe to call on a frame with none.
#[test]
fn a_plate_identifies_by_button_plus_the_border_texture() {
let mut s = vm();
drive(&mut s, &[plate("Wolf", 30.0, 40.0)]);
assert!(s
.eval::<bool>(
r#"
local plate = WorldFrame:GetChildren()
if plate:GetObjectType() ~= "Button" then return false end
local region = plate:GetRegions()
if not region or not region.GetTexture then return false end
if region:GetObjectType() ~= "Texture" then return false end
return region:GetTexture() == "Interface\\Tooltips\\Nameplate-Border"
"#
)
.unwrap());
}
/// The two tuples, positionally — the ABI decision 2148 §3 records.
///
/// `CustomNameplates.lua:212` binds `Border, Glow, Name, Level, Boss, RaidTargetIcon`; pfUI and
/// ShaguTweaks bind the same six by the `NAMEPLATE_OBJECTORDER` table. A **seventh** region is a
/// bug (pfUI blanks any extra with `SetTexture("")`), and a **second** child is a bug too (pfUI
/// destructures `healthbar, castbar` and would blank ours as a TBC cast bar).
#[test]
fn the_region_and_child_tuples_are_six_and_one() {
let mut s = vm();
drive(&mut s, &[plate("Wolf", 30.0, 40.0)]);
assert_eq!(
s.eval::<i64>("local p = WorldFrame:GetChildren() return p:GetNumRegions()")
.unwrap(),
6
);
assert_eq!(
s.eval::<i64>("local p = WorldFrame:GetChildren() return p:GetNumChildren()")
.unwrap(),
1
);
assert!(s
.eval::<bool>(
r#"
local p = WorldFrame:GetChildren()
local border, glow, name, level, levelicon, raidicon = p:GetRegions()
return border:GetTexture() == "Interface\\Tooltips\\Nameplate-Border"
and glow:GetTexture() == "Interface\\Tooltips\\Nameplate-Glow"
and name:GetObjectType() == "FontString"
and level:GetObjectType() == "FontString"
and levelicon:GetTexture() == "Interface\\TargetingFrame\\UI-TargetingFrame-Skull"
and raidicon:GetTexture() == "Interface\\TargetingFrame\\UI-RaidTargetingIcons"
"#
)
.unwrap());
// ShaguTweaks `libnameplate.lua:39`: `plate.healthbar = plate:GetChildren()` — one child, and
// it is the StatusBar. Its own single region is the fill `GetStatusBarTexture()` hands back.
assert!(s
.eval::<bool>(
r#"
local p = WorldFrame:GetChildren()
local healthbar = p:GetChildren()
if healthbar:GetObjectType() ~= "StatusBar" then return false end
if healthbar:GetNumRegions() ~= 1 then return false end
local fill = healthbar:GetStatusBarTexture()
return fill:GetTexture() == "Interface\\TargetingFrame\\UI-TargetingFrame-BarFill"
"#
)
.unwrap());
}
/// `_Nameplates.Nameplate.Initialize` (`_Nameplates.lua:163`) is the strictest consumer: it
/// content-addresses the regions and **validates the two FontString anchors**, rejecting any plate
/// whose name is not `BOTTOM`→`CENTER` and whose level is not `CENTER`→`BOTTOMRIGHT`. Those are the
/// reference's own (ctor `0x7cb456`, bind `0x7cb7f5`).
#[test]
fn the_two_fontstring_anchors_are_what_nameplates_validates() {
let mut s = vm();
drive(&mut s, &[plate("Wolf", 30.0, 40.0)]);
assert!(s
.eval::<bool>(
r#"
local plate = WorldFrame:GetChildren()
local Name, Level
for _, region in ipairs({ plate:GetRegions() }) do
if region:GetObjectType() == "FontString" then
local point, _, relativePoint = region:GetPoint()
if point == "BOTTOM" and relativePoint == "CENTER" then Name = region end
if point == "CENTER" and relativePoint == "BOTTOMRIGHT" then Level = region end
end
end
return Name ~= nil and Level ~= nil
"#
)
.unwrap());
}
/// The bar reports **raw** health, not a fraction — `GetValue 0x78f5d0` → `[bar+0x320]`, and
/// `GetMinMaxValues` → `(0, maxHealth)`. pfUI (`:595`) and CustomNameplates (`:471`) both divide,
/// so a fraction here would read as full health on every plate.
///
/// This one corrected wow-re's own note: `nameplate-vkey.md` §4 had recorded `0x783380`, which is
/// the internal fill *fraction*, as the Lua getter.
#[test]
fn the_healthbar_reports_raw_health_and_its_max() {
let mut s = vm();
drive(&mut s, &[plate("Wolf", 30.0, 40.0)]);
assert!(s
.eval::<bool>(
r#"
local healthbar = WorldFrame:GetChildren():GetChildren()
local min, max = healthbar:GetMinMaxValues()
return healthbar:GetValue() == 30 and min == 0 and max == 40
"#
)
.unwrap());
}
/// The plate is anonymous. `_Nameplates.lua:164` rejects any WorldFrame child with a name, and
/// pfUI's bubble filter does the same — a named plate is invisible to both.
#[test]
fn a_plate_is_anonymous() {
let mut s = vm();
drive(&mut s, &[plate("Wolf", 30.0, 40.0)]);
assert!(s
.eval::<bool>("return WorldFrame:GetChildren():GetName() == nil")
.unwrap());
}
/// The pooling contract: a retired plate comes back as the **same Lua object**. Every addon keys
/// its per-plate state off exactly that identity — pfUI's `registry[plate]` (`:342`),
/// `_Nameplates.Frames[Nameplate]` (`:205`) — and re-runs `OnCreate` for an object it has not seen.
#[test]
fn a_retired_plate_returns_as_the_same_lua_object() {
let mut s = vm();
drive(&mut s, &[plate("Wolf", 30.0, 40.0)]);
s.run("FirstPlate = WorldFrame:GetChildren()").unwrap();
drive(&mut s, &[]);
drive(&mut s, &[plate("Bear", 10.0, 90.0)]);
assert!(s
.eval::<bool>("return WorldFrame:GetChildren() == FirstPlate")
.unwrap());
}
/// **A plate belongs to its UNIT, not to this frame's sort order.**
///
/// The driver hands plates over in distance-sorted order — the seat priority the reference solves
/// overlap in — and that order changes whenever two units cross. If the pool were indexed by it,
/// widget #1 would swap units mid-session, and every addon that caches state on the frame would be
/// reading the other unit's: pfUI's `nameplate.original.name`/`level`, its `registry[plate]`
/// bookkeeping, `_Nameplates`' per-plate button. So the pool is keyed by the unit's GUID, and this
/// is the test that says so.
#[test]
fn a_plate_belongs_to_its_unit_across_a_sort_order_swap() {
let mut s = vm();
drive(
&mut s,
&[plate("Wolf", 30.0, 40.0), plate("Bear", 10.0, 90.0)],
);
s.run(
r#"
WolfPlate = nil
for _, p in ipairs({ WorldFrame:GetChildren() }) do
local _, _, name = p:GetRegions()
if name:GetText() == "Wolf" then WolfPlate = p end
end
"#,
)
.unwrap();
assert!(s.eval::<bool>("return WolfPlate ~= nil").unwrap());
// The two units cross: the driver now sorts Bear first. Nothing about either plate's identity
// may move with it.
drive(
&mut s,
&[plate("Bear", 10.0, 90.0), plate("Wolf", 30.0, 40.0)],
);
assert!(s
.eval::<bool>(
r#"
local _, _, name = WolfPlate:GetRegions()
return name:GetText() == "Wolf"
"#
)
.unwrap());
}
/// `plate:GetAlpha() == 1` is how pfUI (`:879`) and CustomNameplates (`:287`) detect the TARGET
/// plate: with a target up, every other plate is dimmed. So alpha is a contract, not a look.
#[test]
fn the_target_plate_is_the_opaque_one() {
let mut s = vm();
let mut target = plate("Wolf", 30.0, 40.0);
let mut other = plate("Bear", 10.0, 90.0);
target.alpha = 1.0;
other.alpha = 0.5;
drive(&mut s, &[target, other]);
assert!(s
.eval::<bool>(
"local a, b = WorldFrame:GetChildren() return a:GetAlpha() == 1 and b:GetAlpha() < 1"
)
.unwrap());
}
/// pfUI hooks the bar's `OnValueChanged` (`:393`) and reads `this:GetParent()` inside the handler
/// to find the plate. The driver moves that value from Rust, so the handler has to fire from there
/// too — the reference's own bar is driven by a GUID-watch callback and fires it the same way.
#[test]
fn an_engine_health_write_fires_onvaluechanged_with_the_plate_as_parent() {
let mut s = vm();
drive(&mut s, &[plate("Wolf", 30.0, 40.0)]);
s.run(
r#"
Fired = 0
ParentIsPlate = false
local plate = WorldFrame:GetChildren()
local healthbar = plate:GetChildren()
healthbar:SetScript("OnValueChanged", function()
Fired = Fired + 1
ParentIsPlate = (this:GetParent() == plate)
end)
"#,
)
.unwrap();
drive(&mut s, &[plate("Wolf", 12.0, 40.0)]);
assert_eq!(s.eval::<i64>("return Fired").unwrap(), 1);
assert!(s.eval::<bool>("return ParentIsPlate").unwrap());
// Unmoved health fires nothing: the driver writes only what changed.
drive(&mut s, &[plate("Wolf", 12.0, 40.0)]);
assert_eq!(s.eval::<i64>("return Fired").unwrap(), 1);
}
/// **An addon takes the plate over.** pfUI blanks all six regions (`DisableObject` →
/// `SetTexture("")`) and paints its own art; ShaguTweaks reparents the raid icon. A driver that
/// re-stamped every property every frame would undo that at 60 Hz — so static properties are
/// written once, at creation, and never again.
#[test]
fn the_driver_does_not_overwrite_an_addons_takeover() {
let mut s = vm();
drive(&mut s, &[plate("Wolf", 30.0, 40.0)]);
s.run(
r#"
local plate = WorldFrame:GetChildren()
for _, region in ipairs({ plate:GetRegions() }) do
if region.SetTexture then region:SetTexture("") end
end
"#,
)
.unwrap();
// Several frames of ordinary driving — health moving, the unit renamed, the plate re-seated.
for hp in [29.0, 28.0, 27.0] {
let mut state = plate("Wolf", hp, 40.0);
state.top_centre = (500.0 + hp, 400.0);
drive(&mut s, &[state]);
}
assert!(s
.eval::<bool>(
r#"
local border = WorldFrame:GetChildren():GetRegions()
return border:GetTexture() == "" or border:GetTexture() == nil
"#
)
.unwrap());
}
/// pfUI reads `glow:IsShown()` as the MOUSEOVER signal (`:601`, `:880`). benilla does not paint the
/// additive rim (0184 — it read as hard edge lines on our pipeline, and the director pinned a bar
/// brighten instead), but the region is structurally present and really shown, because the
/// deviation is about pixels and this is about data.
#[test]
fn the_glow_region_is_the_mouseover_signal() {
let mut s = vm();
drive(&mut s, &[plate("Wolf", 30.0, 40.0)]);
assert!(s
.eval::<bool>(
"local _, glow = WorldFrame:GetChildren():GetRegions() return glow:IsShown() == nil"
)
.unwrap());
let mut hovered = plate("Wolf", 30.0, 40.0);
hovered.hovered = true;
drive(&mut s, &[hovered]);
assert!(s
.eval::<bool>(
"local _, glow = WorldFrame:GetChildren():GetRegions() return glow:IsShown() == 1"
)
.unwrap());
}
/// The level number and the skull share one seat and are mutually exclusive — `level = None` is
/// how the driver says "world boss, or ten levels up". pfUI gates on `levelicon:IsShown()` and
/// CustomNameplates on `Boss:IsVisible()`, so both flags have to move.
#[test]
fn the_skull_replaces_the_level_number() {
let mut s = vm();
let mut skulled = plate("Wolf", 30.0, 40.0);
skulled.level = None;
drive(&mut s, &[skulled]);
assert!(s
.eval::<bool>(
r#"
local _, _, _, level, levelicon = WorldFrame:GetChildren():GetRegions()
return level:IsShown() == nil and levelicon:IsShown() == 1
and levelicon:IsVisible() == 1
"#
)
.unwrap());
}
/// An addon's own frame parented to `WorldFrame` shares the list with the plates —
/// `_NameplatesFrame` and `pfUICombatScreen` both are — which is why the identification test in
/// §2 is load-bearing and why the count-grew heuristic re-scans. It must not be mistaken for one.
#[test]
fn an_addon_frame_under_the_worldframe_is_not_a_plate() {
let mut s = vm();
drive(&mut s, &[plate("Wolf", 30.0, 40.0)]);
s.run(r#"CreateFrame("Frame", "pfUICombatScreen", WorldFrame)"#)
.unwrap();
assert_eq!(
s.eval::<i64>("return WorldFrame:GetNumChildren()").unwrap(),
2
);
assert_eq!(
s.eval::<i64>(
r#"
local found = 0
for _, frame in ipairs({ WorldFrame:GetChildren() }) do
local region = frame:GetRegions()
if frame:GetObjectType() == "Button" and region and region.GetTexture
and region:GetTexture() == "Interface\\Tooltips\\Nameplate-Border" then
found = found + 1
end
end
return found
"#
)
.unwrap(),
1
);
}
/// **The plate does NOT claim the UI pointer — not yet, and deliberately.**
///
/// The reference's plate is a mouse-enabled `Button`: hovering it publishes the mouseover
/// (`0x7cb850` OnEnter → `0x492890`) and a click selects the unit through the button's own click
/// slot. benilla cannot simply set that bit, because on this engine the UI pointer pass owning a
/// frame is what makes `PointerOverUi` true, and `target::hover::update_hover` returns early on
/// exactly that — so a mouse-enabled plate with no engine-side seam takes the hover and gives
/// nothing back: no model brighten, no lit bar, and no selection on click, because the click never
/// reaches the world path either.
///
/// So the bit stays off until the seam exists (2148 §8), and this test is the tripwire: turning it
/// on without wiring the hover and click through the engine fails here, in the ABI suite, rather
/// than under the director's cursor.
#[test]
fn a_plate_does_not_yet_claim_the_ui_pointer() {
let mut s = vm();
drive(&mut s, &[plate("Wolf", 30.0, 40.0)]);
// The plate's TOP-CENTRE is (500, 400) and it hangs 32 units below, so this is its middle.
// `mouse_move` speaks the same y-up FrameXML space the anchors do.
//
// The WorldFrame itself IS a legitimate hit here — it is mouse-enabled by construction, and
// the app filters exactly that one id out (`input::feed_ui_input`, decision 1983) so camera
// look, world clicks and hover targeting stay live over it. So the assertion mirrors the
// app's own filter: whatever the pointer lands on must be the WorldFrame, never a plate.
let focus = s.mouse_move(500.0, 384.0);
assert!(
focus.is_none_or(|id| s.is_world_frame(id)),
"a plate claimed the UI pointer: the hover and click seams (2148 §8) have to land in the \
same change that enables the mouse, or plate hover and click-select both die"
);
}