Files
benilla-octo/crates/benilla-ui/src/script/nameplate.rs
T
SamandClaude Fable 5.1 38e45badb9 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>
2026-09-11 20:55:15 +07:00

669 lines
32 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! **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;