Files
benilla-octo/crates/benilla-app/src/vplates.rs
T
SamandClaude Fable 5.1 3b084ba052 pfUI's hdgraphic runs, the plates settle, and the water camera comes out
- pfUI's hdgraphic module runs: SetWorldDetail and GetWorldDetail are real verbs with the reference's three stops, and the near clip plane is a live setting at 0.100 yd rather than a frozen 0.111 (2163)
- nameplates stop drawing a black box on hover or click, stop jittering a frame behind a moving creature, and a unit that walks into range later gets its plate; a unit with no level wears no skull, and the wheel zooms over a plate (2168)
- the faint one-pixel box around every letter of a magnified overhead name is gone, each glyph now carrying its own transparent padding; interface text drawn at 1:1 is byte-identical (2169)
- the camera's water collision comes back out for now: its pivot corridor dropped the camera a yard in one frame at head depth, which is the snapping reported around the surface while swimming (2165)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 21:02:02 +07:00

1129 lines
61 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.
//! **V-key nameplates** — `CGNamePlateFrame` (wow-re `object-layer/scratch/nameplate-vkey.md`,
//! §5-verified 2026-07-06): the toggled health-bar plates over units' heads, a **2-D overlay**
//! distinct from the world-pass overhead *names* (`crate::nameplates`) they replace.
//!
//! The pinned law, transcribed:
//! - **Master toggles** (`[0xc4da34]` bit 0 enemy / bit 3 friendly; real-client boot default
//! **OFF for both**, and since 1804 ours too — [`VPlateMode::default`]): the
//! `ShowNameplates`/`ShowFriendNameplates` script pairs —
//! bound to **V / Shift-V** by FrameXML `Bindings.xml` (asset-sourced default, like TAB).
//! - **Gate**: never the own unit; never `NOT_SELECTABLE` (UNIT_FIELD_FLAGS bit 25); the
//! enemy/friendly split is **`CanAttack(localPlayer → unit)`**, NOT a reaction threshold
//! ([`crate::target::ring::plate_is_friendly`], wow-re `nameplate-category-gate.md` §2/§3,
//! §5-VERIFIED 2026-08-22 — this file shipped `rank >= 4` for a year and 1530 corrects it) —
//! and a **player** subject must additionally pass `CanCooperate`, which is nothing but the two
//! `FactionTemplate` faction-group masks being equal, so a player whose row carries neither
//! side's mask (a vmangos `.gm on` character is template 35, mask 0) is enemy-category no matter
//! how friendly they read; **max 20 yd**, hardcoded; **no
//! occlusion** (a 2-D overlay — plates draw through walls); snap, no smoothing. Anti-overlap
//! there *is*, since 0367 found the shared solver ([`crate::smart_rect`]) — this file's older
//! "no anti-overlap" was the census that missed it. And the sphere is bounded by the frustum:
//! the unit must be **in view** ([`crate::ui_pass::project_overlay`]) or the reference
//! **destroys** the plate outright (`0x60f600`, before the 20-yard cull — wow-re
//! `nameplate-offscreen-cull.md`, §5-VERIFIED 2026-08-15); it is never clamped in from
//! off-screen, which is what ours did (1341/1344).
//! (The friendly-totem exclusion waits on totems existing.)
//! - **Anchor**: the same overhead head point (`0x608640`, [`overhead_anchor`]) **+ 2/3 yd**
//! (`[0x80abfc]`), projected per frame (`0x483ee0`); the plate's **TOP-CENTER** lands on the
//! point (it hangs below — §8 Q5) and is edge-clamped half a plate inside the screen;
//! **constant screen size**: the geometry globals `[0x87d9cc]=0.1` × `[0x87d9d0]=0.025` in gx
//! screencoord units, where one unit = the screen **diagonal** `√(W²+H²)` (§8 Q4 — and never
//! uiScale: plates live outside the UIParent cascade). **Our basis is growth-damped** past
//! [`PLATE_DIAG_KNEE`] — a director-pinned deviation (0185/0186): the real client grows
//! plates diagonal-linear without limit (§9, byte-closed) and the director rejected that
//! look; past 1024×768 the plate grows at half the real rate (midway between faithful and
//! the native size).
//! - **Anatomy — not here any more.** The plate's six regions, its health-bar child, their
//! layers and the anchors between them live with the widgets that carry them
//! ([`benilla_ui::script::nameplate`], decision 2148): the plate is a real `Button` under the
//! `WorldFrame` and the shared frame→quad path draws it. What stays this file's is everything
//! about the WORLD — the gate above, the anchor, the projection, the seat, and the damped
//! basis the geometry is computed in ([`plate_basis`], [`gx_px`], [`text_px`]).
//! - **Highlight = the mouseover unit ∪ the target** (the watcher `0x606f20 → 0x607080` reads
//! both globals): the bar's own colour brightens — a uniform [`LIT_BOOST`] lift of the fill
//! tint, gradient untouched, nothing else changes (director-pinned form, 0184; the recorded
//! ADD `Nameplate-Glow` rim read as hard edge lines on our linear-blending pipeline and is
//! not drawn). The plate is itself mouse-enabled UI: hovering it makes its unit the mouseover
//! (OnEnter `0x7cb850` → `[0xb4e2c8]` — [`PlateHover`], read straight off the widget the
//! pointer landed on since 2159), which also lifts the model emissive, and a completed click
//! selects through the plate ([`PlateClicks`]); the hover additionally turns the name yellow
//! `0xFFFFFF00` (OnEnter, name-only).
//! All decoupled from the target dim below.
//! - **Target highlight**: relative alpha — with a target, the target's plate is opaque and
//! every other plate drops to `0x7F`; with no target all are opaque.
//! - **Mutually exclusive with the overhead name** (ShouldShowName): a unit with a live plate
//! never also draws its floating name — [`VPlates`] is that verdict, read by the name driver.
//! (The questgiver marker raises for a live plate too — a director-pinned DEVIATION: the
//! reference really does sit low under a plate (byte-verified, wow-re `questgiver-marker.md`
//! Q4a) and the director rejected that overlap; rationale on `quest_markers::pose_markers`,
//! 0408/0409.)
//!
//! (The skull's trivial-gray leg is the shared grey check, [`benilla_ui::script::unit_is_grey`]
//! — `0x5f0700`, §5-VERIFIED 2026-07-17, the same one the tooltip/quest-range APIs read; it is
//! vacuous on a ≥ +10 hostile and kept for transcription fidelity.)
use bevy::ecs::entity::EntityHashSet;
use bevy::ecs::system::SystemParam;
use bevy::prelude::*;
use benilla_protocol::EntityKind;
use benilla_ui::script::{unit_is_grey, PlateGeometry, PlateState};
use crate::entities::{overhead_anchor, BoneAttach, OverheadFallback};
use crate::names::NameCache;
use crate::net::{Guid, NetCommands, NetEntity, ObjectStore, Reputations, SelfPlayer};
use crate::target::{ring_reaction, Factions, Hovered, Selection, TargetUpdate};
use benilla_world::view::WorldCamera;
/// Sharp-resampling the frame border ([`border::resample_sharp`]) so the 128×32 art reads crisp at
/// the plate's larger size instead of bilinear-magnified soft (0188).
pub(crate) mod border;
/// The master bitmask (`[0xc4da34]`): bit 0 enemy plates, bit 3 friendly. **Both boot OFF, the
/// real client's own state** — see the derivation below. Enemy plates booted ON here from the 0167
/// director call (combat plates from the first frame) until 1804; friendly has been OFF since the
/// 0599 call, which also gives friendly speech bubbles room, because the faithful
/// plate-blocks-bubble gate would otherwise suppress them. Both stay V / Shift-V togglable.
/// `pub(crate)` so the capture harness can force plates on for the `vplates` scenario.
///
/// **This resource is the truth; the CVars are its persistence** (the [`CVAR_ENEMIES`] /
/// [`CVAR_FRIENDS`] pair, [`crate::cvars`]). That mirrors the reference's own two-store shape: the
/// engine bitmask is what the gate reads, and FrameXML keeps `NAMEPLATES_ON`/`FRIENDNAMEPLATES_ON`
/// beside it for saving (`RegisterForSave`, UIOptionsFrame.lua) — pushing changes back through
/// `ShowNameplates()`/`HideNameplates()`. 1.12 registers **no** nameplate CVar (wow-re, VERIFIED:
/// no such string exists in the binary), so the names are the LATER-era engine's, the same posture
/// as `autoLootDefault` — benilla's persistence lives in the CVar store (0954), and a setting with
/// no 1.12 CVar takes the era name rather than inventing one.
///
/// **Both boot OFF — the reference's own state**, and verified on both of its halves, because
/// either alone would be half an answer: the engine bitmask `[0xc4da34]` starts clear, and
/// FrameXML's `UIOptionsFrame_Init` sets `NAMEPLATES_ON = nil` / `FRIENDNAMEPLATES_ON = nil` and
/// only calls `ShowNameplates()` when the SAVED value is truthy (the install's own
/// `UIOptionsFrame.lua` l.180-183, l.769-775). So a fresh 1.12 client draws no plates until you
/// press V. Enemy plates shipped ON here from 0167 until 1804 — the same class of call as the
/// overhead-name pair (`nameplates::NameConfig`): useful while the feature was being built, never
/// re-weighed as a default. The `Default` is derived now that the claim is "both false", and it is
/// still a *claim*: `cvars::tests` welds it to the registered `nameplateShowEnemies`/`Friendly`
/// defaults and asserts the pair is off in as many words, so the derive cannot drift quietly.
#[derive(Resource, Default, Clone, Copy)]
pub(crate) struct VPlateMode {
pub(crate) enemies: bool,
pub(crate) friends: bool,
}
/// The persisted names of the two toggles — see [`VPlateMode`]. Welded to [`crate::cvars`]'s
/// registered table by its own test, so the spelling here and the row on the Nameplates page can
/// never drift apart.
pub(crate) const CVAR_ENEMIES: &str = benilla_ui::script::CVAR_NAMEPLATE_ENEMIES;
pub(crate) const CVAR_FRIENDS: &str = benilla_ui::script::CVAR_NAMEPLATE_FRIENDS;
/// The units carrying a live plate this frame — the ShouldShowName exclusivity verdict the
/// overhead-name driver reads (a plated unit never also draws its floating name).
#[derive(Resource, Default)]
pub(crate) struct VPlates(pub(crate) EntityHashSet);
/// **The unit whose plate the pointer is inside** — the plate's own OnEnter publishing the
/// mouseover, from this side (`0x7cb850` → `[0xb4e2c8]`).
///
/// The plate is real mouse-enabled UI now (2148), so the UI pointer pass owns the cursor over it
/// and `target::hover`'s world pick correctly stands down; this is how the unit still reaches
/// [`crate::target::Hovered`]. Written by the plate driver from
/// [`benilla_ui::script::UiScript::hovered_nameplate`] — last frame's layout, which is the
/// reference's own input-vs-layout latency.
#[derive(Resource, Default)]
pub(crate) struct PlateHover(pub(crate) Option<Entity>);
/// Completed clicks on plates, waiting for the targeting chain — the reference's plate click slot
/// (`0x7cb910`), which ends in the same `SetSelection` a click on the body does.
///
/// Both a physical click and an addon's `plate:Click("LeftButton")` land here: the engine records
/// them at the one click funnel both go through.
#[derive(Resource, Default)]
pub(crate) struct PlateClicks {
pub(crate) left: Vec<Entity>,
pub(crate) right: Vec<Entity>,
}
/// The plate frame, gx screen-height units (`[0x87d9cc]`/`[0x87d9d0]`): 0.1 × 0.025. The border
/// SetAllPoints-fills it; everything else anchors inside it (§7, byte-verified offsets).
const PLATE_W: f32 = 0.1;
const PLATE_H: f32 = 0.025;
/// The health bar: BOTTOMLEFT ← plate BOTTOMLEFT + (0.0031, 0.003125), sized 0.0804 × 0.007025.
const BAR_OFF_X: f32 = 0.0031;
const BAR_OFF_Y: f32 = 0.003125;
const BAR_W: f32 = 0.0804;
const BAR_H: f32 = 0.007025;
/// Name text height 0.01, its BOTTOM at the plate CENTER; level height 0.0086, its CENTER at
/// plate BOTTOMRIGHT + (−0.0092, +0.0071); both carry the FontString's black drop shadow at
/// ±0.001. The skull (0.01 × 0.01) overlays the level's anchor when it replaces the number.
///
/// `LEVEL_H` is a **director-pinned deviation** (2026-07-07) from the byte 0.009: one em smaller
/// across the window range (em 11 vs 12 at the reference, −1 up through 1440p). TEXT ems compose
/// through the same damped gx basis as the frame geometry ([`text_px`] — the client-side chain is
/// byte-closed end to end, wow-re §9 Q3: live re-raster on resize, no fixed UI space, no staleness).
const NAME_H: f32 = 0.01;
const LEVEL_H: f32 = 0.0086;
const LEVEL_OFF_X: f32 = 0.0092;
const LEVEL_OFF_Y: f32 = 0.0071;
const SKULL_SIZE: f32 = 0.01;
/// The raid-target icon (vkey §7, VERIFIED): 0.02 × 0.02, RIGHT ← border.LEFT (0,0) — it hangs
/// off the plate's left edge, vertically centered; the 4-column atlas cell is 0.25.
const RAID_ICON_SIZE: f32 = 0.02;
/// The plate's world lift above the overhead anchor: 2/3 yd (`[0x80abfc]`).
const PLATE_LIFT: f32 = 2.0 / 3.0;
/// Max plate distance, hardcoded (no cvar): 20 yd.
const MAX_DIST_SQ: f32 = 20.0 * 20.0;
/// The non-target relative alpha while something is targeted — a **director-pinned deviation**
/// (2026-07-07) from the faithful `0x7F` (0.5), which faded the other plates too hard; raised so
/// non-target plates stay readable. Tunable: 0.5 = the byte law, 1.0 = no dim.
const DIM_ALPHA: f32 = 178.0 / 255.0;
/// The bar-fill palette — the byte-confirmed dwords (`0xcf60d0/e8/c8/dc`): pure
/// red/blue/yellow/green (NOT the ring's pale player-blue). Client-space sRGB, the [`UiQuads`]
/// convention.
const PLATE_HOSTILE: [f32; 4] = [1.0, 0.0, 0.0, 1.0];
const PLATE_NEUTRAL: [f32; 4] = [1.0, 1.0, 0.0, 1.0];
const PLATE_FRIENDLY: [f32; 4] = [0.0, 1.0, 0.0, 1.0];
const PLATE_PLAYER: [f32; 4] = [0.0, 0.0, 1.0, 1.0];
/// The plate palette selector — `0x7cbaa0`'s exact test order (§7, VERIFIED dwords): hostile
/// (reaction ≤ 1) red, else a player pure blue, else friendly (≥ 4) green, else — reaction 2–3,
/// unfriendly AND neutral — yellow. (Rank 2 is yellow, not red: an earlier transcription here
/// painted it hostile.) The bar fill (and its lit brighten) tints through this.
fn plate_tint(rank: u8, is_player: bool) -> [f32; 4] {
if rank <= 1 {
PLATE_HOSTILE
} else if is_player {
PLATE_PLAYER
} else if rank >= 4 {
PLATE_FRIENDLY
} else {
PLATE_NEUTRAL
}
}
/// The level con palette — the client's EXACT dwords (§7; the softened WoW colors, not the
/// FrameXML QuestDifficultyColor approximations we shipped first): red `0xFFFF1919`, orange
/// `0xFFFF7F3F`, yellow `0xFFFFFF00`, green `0xFF3FB23F`, gray `0xFF7F7F7F`.
const CON_RED: [f32; 4] = [1.0, 25.0 / 255.0, 25.0 / 255.0, 1.0];
const CON_ORANGE: [f32; 4] = [1.0, 127.0 / 255.0, 63.0 / 255.0, 1.0];
const CON_YELLOW: [f32; 4] = [1.0, 1.0, 0.0, 1.0];
const CON_GREEN: [f32; 4] = [63.0 / 255.0, 178.0 / 255.0, 63.0 / 255.0, 1.0];
const CON_GRAY: [f32; 4] = [127.0 / 255.0, 127.0 / 255.0, 127.0 / 255.0, 1.0];
/// The client's own con-color law (§7, byte-verified — thresholds AND the grayband table
/// `[0x81dda8]`, indexed `playerLevel/5`; it agrees with vmangos' formula at every probed level
/// but the table is the client's own): `diff = unit − player`; ≥ +5 red, +3/+4 orange,
/// −2..+2 yellow, lower → green while within `grayband` levels below, gray past it. The
/// grayband/grey test lives in ONE home ([`benilla_ui::script::unit_is_grey`], the byte-identical
/// `0x80ae98`/`0x81dda8` twins) shared with the engine tooltip's "??"/CIVILIAN gates and the
/// `GetQuestGreenRange` binding — so the plate, the tooltip, and the target frame can never
/// disagree on where grey starts.
fn con_color(pl_level: u32, unit_level: u32) -> [f32; 4] {
let diff = i64::from(unit_level) - i64::from(pl_level);
if diff >= 5 {
CON_RED
} else if diff >= 3 {
CON_ORANGE
} else if diff >= -2 {
CON_YELLOW
} else if !unit_is_grey(pl_level, unit_level) {
CON_GREEN
} else {
CON_GRAY
}
}
/// Snap a logical-pixel coordinate onto the **device** pixel grid — the plate's texel alignment
/// (a benilla divergence: the reference draws at fractional device coords).
///
/// The plate rect is snapped so the sharp-resampled border art blits 1:1 and reads crisp instead
/// of bilinear-smeared (0188). Alignment is a property of the **framebuffer**, though, and the
/// quad lane is *logical* px ([`crate::ui_pass`]: 1 world unit = 1 logical px) — so the plain
/// `round()` this shipped with quantized the plate to `scale_factor` PHYSICAL pixels: two pixels
/// of stepping on the 2× display we develop and play on, against a world that slides continuously
/// underneath. At a fractional scale (1.25/1.5, the Windows norm) it did not land on a texel
/// boundary at all — the crispness it was bought for was an accident of an integer scale factor.
///
/// Snapping on the device grid keeps the 1:1 blit, is correct at any scale factor, and halves the
/// stepping at 2×: measured over a Goldshire walk at 1440×810 (the `vpl` trace), the extra
/// displacement the snap adds to a plate's glide fell from a median 0.33 / max 0.99 logical px per
/// frame to 0.21 / 0.49. What is left is the ±½ device pixel every crisp UI pays.
///
/// **Shared with the chat bubble** ([`crate::chat_bubble`]), like [`plate_basis`]/[`gx_px`]: it is
/// the same outside-UIParent overlay family sliding over the same world, so it is the same snap
/// law. The bubble shipped on a plain logical `round()` — this function's own bug, at the one site
/// that never got the fix — which is 1398's finding.
pub(crate) fn device_snap(v: f32, scale: f32) -> f32 {
(v * scale).round() / scale
}
/// The knee of the plate's gx basis — **a director-pinned DEVIATION from the byte law**
/// (0185/0186). The real client's plates grow diagonal-linear without limit (wow-re §9,
/// byte-closed: ~294 px plate + em-29 name at 2560×1440, bilinear-softened) — the director
/// rejected that look as too big/thick/soft, and a hard cap at native (0185) as too small.
/// 1280 is the diagonal where the 0.1 × 0.025 frame is EXACTLY the border art's native
/// 128 × 32 px; past it the basis grows at [`PLATE_GROWTH_DAMP`] of the real rate — at every
/// window the plate lands midway between the faithful size and the native pin.
const PLATE_DIAG_KNEE: f32 = 1280.0;
/// The growth rate past the knee: ½ = the midpoint between the byte law and the 0185 native
/// pin (the director's "between what it was and what it is now"). 0 restores the 0185 pin,
/// 1 the faithful law.
const PLATE_GROWTH_DAMP: f32 = 0.5;
/// One gx screencoord unit = the screen **DIAGONAL** `√(W²+H²)` (§8 Q4 — the device space spans
/// `[0,G44]×[0,G48]`, the live aspect basis; uiScale never enters: plates anchor WorldFrame-side,
/// outside the UIParent cascade) — growth damped past [`PLATE_DIAG_KNEE`] (the director's pin).
/// Shared with the chat bubble ([`crate::chat_bubble`]) — the sibling outside-UIParent overlay,
/// same basis law so bubble text and plate text hold the same em at every window.
pub(crate) fn plate_basis(viewport: Vec2) -> f32 {
let d = viewport.x.hypot(viewport.y);
if d <= PLATE_DIAG_KNEE {
d
} else {
PLATE_DIAG_KNEE + (d - PLATE_DIAG_KNEE) * PLATE_GROWTH_DAMP
}
}
/// gx screencoord units → pixels over the plate basis. Frame geometry only — text takes
/// [`text_px`]. Shared with the chat bubble, like [`plate_basis`].
pub(crate) fn gx_px(v: f32, basis: f32) -> f32 {
(v * basis).round()
}
/// Plate FontString height → the FreeType em: `min(32, round(h · basis))`. The real client's
/// law is **byte-closed end to end** (wow-re `25cfa33e` + §9 Q3: the FontObject bridge
/// `0x44d040` divides the gx height by G48 before `GxuFontCreate`, the raster `0x5ca030`
/// multiplies by the viewport height — netting `h·√(W²+H²)`, re-rasterized live on resize,
/// 32-capped at the atlas cell); our `basis` is that diagonal under the director's
/// [`PLATE_DIAG_KNEE`] damping. Reference-pinned at its 1152×648 window: name em 13, level 12.
/// Shared with the chat bubble (`NAMEPLATE_FONT` at the same 0.01 gx), like [`plate_basis`].
pub(crate) fn text_px(h: f32, basis: f32) -> f32 {
(h * basis).round().min(32.0)
}
/// **The FrameXML mirror of the two toggles** — `NAMEPLATES_ON` and `FRIENDNAMEPLATES_ON`
/// (decision 2132), pushed into whichever VM is live.
///
/// The reference keeps the plate state in **two** levels: the engine bitmask `[0xc4da34]`, which
/// is volatile and cleared on every `EnterWorld`, and those two FrameXML globals, which are the
/// saved store. `UpdateNameplates` (`UIOptionsFrame.lua` l.768) replays the store into the
/// bitmask, from `UIParent_OnEvent`'s VARIABLES_LOADED and PLAYER_ENTERING_WORLD arms
/// (`UIParent.lua` l.234, l.367).
///
/// **benilla has only one level.** [`VPlateMode`] is the state and the [`CVAR_ENEMIES`] /
/// [`CVAR_FRIENDS`] pair is its persistence, and nothing clears it at a world entry — so on this
/// engine the replay has nothing to restore and everything to break. With the globals left nil
/// (nothing here ever wrote them) `UpdateNameplates` took its else branch and called
/// `HideNameplates()`, which writes the CVar, which IS the store: the player's setting was erased
/// at every world entry and `config.toml` lost the line as "at default".
///
/// So the globals are kept TRUE, which makes the stock replay a value-preserving no-op — and is
/// what a third-party addon reading `NAMEPLATES_ON` is owed anyway, which was 2115's whole
/// argument for loading the stock window in the first place.
pub(crate) fn push_plate_globals(script: &benilla_ui::script::UiScript, mode: VPlateMode) {
// The reference's own truthiness for these two: the number `1`, or nil. Never `0` — a Lua
// `0` is truthy, so `NAMEPLATES_ON = 0` would read as ON in `UpdateNameplates`'s `if`.
let on = |b: bool| b.then_some(1i64);
let g = script.lua().globals();
if let Err(e) = g
.set("NAMEPLATES_ON", on(mode.enemies))
.and_then(|()| g.set("FRIENDNAMEPLATES_ON", on(mode.friends)))
{
warn!("nameplates: FrameXML globals: {e}");
}
}
/// The globals kept in step with the mode for the life of each VM ([`push_plate_globals`]).
///
/// Behind a [`crate::ui_script::VmMemo`] (1290) because "this VM has been told" is a fact about
/// the VM, not about the process. The world-entry load seeds them earlier still — ahead of
/// `VARIABLES_LOADED`, which no `Update` system can reach — so this is the steady-state half:
/// a V press, an options checkbox, a `/console` write, an addon's `SetCVar`.
fn feed_plate_globals(
script: Option<NonSendMut<benilla_ui::script::UiScript>>,
mode: Res<VPlateMode>,
mut told: Local<crate::ui_script::VmMemo<Option<(bool, bool)>>>,
) {
let Some(script) = script else {
return;
};
let now = (mode.enemies, mode.friends);
let told = told.get(&script);
if *told != Some(now) {
*told = Some(now);
push_plate_globals(&script, *mode);
}
}
/// NAMEPLATES / FRIENDNAMEPLATES / ALLNAMEPLATES through the binding table (0997; defaults V /
/// SHIFT-V / CTRL-V). The typing gate and 0585's modifier law live in the dispatch now.
///
/// The V and SHIFT-V halves stay benilla's **independent** toggles (the shipped behavior) rather
/// than 1.12's exclusive dance (whose NAMEPLATES body also force-hides friendly plates — a
/// recorded divergence, 0997 residue). ALLNAMEPLATES arrives with the table and takes the 1.12
/// body's semantics as-is: both on unless both already on, else both off.
///
/// **A key press mirrors into the CVar table** ([`VPlateMode`]'s doc): `set_cvar_engine` is the
/// minimap-zoom pattern — the engine's own value moved, so the table follows it AND queues the
/// change, which is what dirties `config.toml` and what makes the Nameplates page's checkboxes
/// read right the next time they are opened. The resource stays authoritative either way: with no
/// UI VM (a capture, a bare test app) the mirror is a silent no-op and V still works.
fn toggle_vplates(
binds: Res<crate::bindings::BindingsState>,
mut mode: ResMut<VPlateMode>,
script: Option<NonSendMut<benilla_ui::script::UiScript>>,
) {
use crate::bindings::cmd;
let (was_enemies, was_friends) = (mode.enemies, mode.friends);
if binds.fired(cmd::NAMEPLATES) {
mode.enemies = !mode.enemies;
info!(
"nameplates: enemy {}",
if mode.enemies { "ON" } else { "OFF" }
);
}
if binds.fired(cmd::FRIEND_NAMEPLATES) {
mode.friends = !mode.friends;
info!(
"nameplates: friendly {}",
if mode.friends { "ON" } else { "OFF" }
);
}
if binds.fired(cmd::ALL_NAMEPLATES) {
let both = mode.enemies && mode.friends;
mode.enemies = !both;
mode.friends = !both;
info!("nameplates: all {}", if !both { "ON" } else { "OFF" });
}
if let Some(mut script) = script {
let flag = |b: bool| if b { "1" } else { "0" };
if mode.enemies != was_enemies {
script.set_cvar_engine(CVAR_ENEMIES, flag(mode.enemies));
}
if mode.friends != was_friends {
script.set_cvar_engine(CVAR_FRIENDS, flag(mode.friends));
}
}
}
/// The world/reaction inputs of the plate gate, bundled (the 16-param ceiling).
#[derive(SystemParam)]
#[allow(clippy::type_complexity)] // one bundled system param — the app's convention
struct PlateWorld<'w, 's> {
units: Query<
'w,
's,
(
Entity,
&'static NetEntity,
&'static Guid,
&'static Transform,
Option<&'static ObjectStore>,
),
Without<SelfPlayer>,
>,
self_q: Query<'w, 's, (&'static Transform, Option<&'static ObjectStore>), With<SelfPlayer>>,
factions: Option<Res<'w, Factions>>,
reputations: Res<'w, Reputations>,
selection: Res<'w, Selection>,
// The mouseover unit (the 3-D pick ∪ last frame's plate rects) — with `selection`, the
// highlight's trigger pair (the watcher's two globals `[0xb4e2c8]`/`[0xb4e2d8]`).
hovered: Res<'w, Hovered>,
camera: Query<'w, 's, (&'static Camera, &'static Transform), With<WorldCamera>>,
// The cursor, for the plate-rect hover (OnEnter — the yellow name, this frame's rects).
window: Query<'w, 's, &'static Window, With<bevy::window::PrimaryWindow>>,
}
/// Gate + draw, every frame: decide which units carry a plate (into [`VPlates`], the
/// name-exclusivity verdict), seat each one, and hand the result to the widget layer as
/// [`PlateState`] — at constant screen size over the projected anchor + 2/3 yd. Runs after
/// the script extract), after the targeting chain (it reads the frame's selection verdict).
#[allow(clippy::too_many_arguments, clippy::type_complexity)] // one Bevy system's full input set
fn drive_vplates(
mode: Res<VPlateMode>,
mut plates: ResMut<VPlates>,
mut plate_hover: ResMut<PlateHover>,
mut plate_clicks: ResMut<PlateClicks>,
// Camera freelook, for the mouselook toggle `0x60f830`: plates stop taking the mouse while
// the pointer is driving the camera.
rig: Res<crate::player::CameraControl>,
world: PlateWorld,
mut names: ResMut<NameCache>,
net_commands: Res<NetCommands>,
// The widget layer this drives (decision 2148). `None` in a VM-less run (a capture with the
// interface off, a bare test app) — the gate below then costs one early return.
script: Option<NonSendMut<benilla_ui::script::UiScript>>,
// The seam this frame's px↔unit conversion runs at (`windowH/768 × uiScale`).
ui_scale: Res<crate::ui_script::UiScaleCvar>,
// The overhead-anchor inputs ([`overhead_anchor`]).
anchor_q: (
Query<&BoneAttach>,
Query<&benilla_world::rig_anim::RigPose>,
Query<&OverheadFallback>,
Query<&GlobalTransform>,
Query<(), With<crate::entities::mount::MountChild>>,
),
// The frame's claim bucket 0 (nameplates) — cleared and rebuilt every pass; worldtext owns
// bucket 1 in `combat_text` and the two never interact (a plate never pushes a number).
mut bucket: Local<crate::smart_rect::SmartBucket>,
// The raid-target board (decision 0434 §6) — the plate's raid-icon child reads it.
group: Res<crate::ui_party::GroupState>,
// Whether this VM's plates have been told about freelook (the `0x60f830` edge).
mut mouse_told: Local<crate::ui_script::VmMemo<Option<bool>>>,
) {
plates.0.clear();
plate_hover.0 = None;
bucket.clear();
// **Every early return has to RETIRE the plates first, and that is new with 2148.** A painter
// could stop drawing and the plates were gone with the frame's quads; widgets stay until they
// are hidden, so a V press that turns plates off — or a camera-less frame, or a world exit —
// would otherwise leave the last frame's plates standing on screen forever. `sync` with no
// states is exactly the reference's own answer: `0x608a10` on every live plate, hiding each and
// returning it to the pool.
let retire_all = |script: Option<NonSendMut<benilla_ui::script::UiScript>>| {
if let Some(mut script) = script {
script.retire_nameplates();
}
};
if !mode.enemies && !mode.friends {
retire_all(script);
return;
}
let (Ok((cam, cam_pose)), Ok((self_tf, self_store))) =
(world.camera.single(), world.self_q.single())
else {
retire_all(script);
return;
};
let Some(mut script) = script else {
return;
};
let cam_tf = GlobalTransform::from(*cam_pose);
let Some(viewport) = cam.logical_viewport_size() else {
script.retire_nameplates();
return;
};
let basis = plate_basis(viewport);
let gx = |v: f32| gx_px(v, basis);
let window = world.window.single().ok();
// **The mouselook toggle** (`0x60f830`, called from `0x483e80`/`0x483e70`): plates stop taking
// the mouse while the camera is in freelook, so a right-drag that starts over a plate turns the
// camera instead of clicking the plate, and the plates are not holding a pointer that has left
// the screen. Written on the EDGE, not per frame — the reference's own is two call sites on the
// freelook transitions, and the memo is keyed to the VM because that is what it is memory about
// (1290). A fresh VM's plates are born with the bit, so the memo starting empty is right.
// Which unit's plate the pointer is inside (last frame's layout), and the completed clicks
// waiting on the targeting chain. Both come from the plate widgets themselves, which is the
// reference's own arrangement: the plate's OnEnter publishes the mouseover and its click slot
// ends in `SetSelection`.
let looking = rig.is_looking();
if *mouse_told.get(&script) != Some(looking) {
*mouse_told.get(&script) = Some(looking);
script.set_nameplate_mouse(!looking);
}
let hovered_key = script.hovered_nameplate();
let clicked = script.take_nameplate_clicks();
let my_level = self_store.and_then(|s| s.0.unit_level()).unwrap_or(1);
let has_target = world.selection.target.is_some();
let (pw, ph) = (gx(PLATE_W), gx(PLATE_H));
let scale = window.map_or(1.0, |w| w.scale_factor());
// The px↔unit seam. Every number below is computed in window px exactly as it always was —
// the projection, the seat, the solve, the device snap — and divided through this once, at
// the boundary, because the widget layer speaks FrameXML units.
let seam = window.map_or(1.0, |w| {
crate::ui_script::seam_scale(w.height(), ui_scale.0)
});
let mut states: Vec<PlateState> = Vec::new();
// The seat PRIORITY (`0x608870`/`0x6089a0` + the per-frame walk `0x608ce0`): plates seat in
// squared-distance order from the fixed gx point (0.4, 0.3) — the 4:3 screen center, a
// literal constant in the ref, converted here over the diagonal basis with the Y mirrored
// out of the ref's Y-up device space. The first-seated plate never moves; each later one is
// solved off the plates already claimed this frame (bucket 0) — the reference's bouncing,
// never-overlapping plates. Gate + project first, then seat in sorted order. The TRUE
// diagonal, not the 0185-damped `basis`: the point is positional law, not plate size.
let diag = viewport.length();
let sort_pt = Vec2::new(0.4 * diag, viewport.y - 0.3 * diag);
let mut cands = Vec::new();
for (entity, net, guid, tf, store) in &world.units {
if !matches!(net.kind, EntityKind::Unit | EntityKind::Player) {
continue;
}
// NOT_SELECTABLE never gets a plate — `0x60f600`'s gate 2 (`0x60f622 shr ecx,0x19` /
// `0x60f628 jne 0x60f740` → pool + hide + clear `[unit+0xe60]`), unconditional, and re-run
// every tick because `0x60f600`'s caller `0x607ef9` sits inside CGUnit's OnUpdate
// (`vtable+0x38` = `0x607ed0`). So the flag arriving on a plated unit takes its plate away
// on the next tick, which falls out of this per-frame gate for free.
//
// **This suppression is load-bearing, not cosmetic** (wow-re
// `object-layer/scratch/not-selectable-mouse-refusal.md`, decision 2060): a plate hover
// publishes the mouseover *directly* — `0x7cb850 OnEnter` → `0x7cb869 call 0x492890`, with
// none of the `IsSelectable` grading the world hover gets at `0x482982`. If a flagged unit
// ever kept its plate, hovering that plate would hand it a name tooltip the reference never
// shows. The plate gate is the only thing standing there.
if store.is_some_and(|s| s.0.unit_flags() & (1 << 25) != 0) {
continue;
}
// A DEAD unit shows no plate — the per-tick gate `0x60f600`'s FIRST, unconditional
// test (§8 Q2, byte-confirmed): signed `UNIT_FIELD_HEALTH ≤ 0`, absent = 0 (the
// zero-init descriptor). A poll, not a death callback; the lootable bit is never
// consulted. Feign death keeps health > 0, so it clears THIS leg — the gate that takes a
// feigning body's plate away is the hostile-only one below (decision 1022).
if store.is_none_or(|s| s.0.unit_health().unwrap_or(0) == 0) {
continue;
}
// …and the SAME per-tick gate's second leg (`0x60f62e`, byte-VERIFIED wow-re
// ghost-death-visuals.md — corrects this file's older "ghost stays plated" note):
// `bytes_1 byte3 & 3` (ghost | creep) denies the plate — a released ghost (health 1)
// carries none.
if store.is_some_and(|s| s.0.unit_is_ghost_visual()) {
continue;
}
// 20 yd, hardcoded, from the player.
if (tf.translation - self_tf.translation).length_squared() > MAX_DIST_SQ {
continue;
}
// A unit with a CREATEDBY owner but no SUMMONEDBY owner, carrying `UNIT_FIELD_FLAGS`
// bit 9 (`0x200`), gets no plate in EITHER category (`0x60f70b`–`0x60f72d`, byte-verified
// — the same CHARMEDBY→SUMMONEDBY→CREATEDBY triple the standalone owner accessor
// `0x611670` walks in the same order). The bit's vanilla NAME is unrecorded; the number
// is what the binary tests, so that is what we test.
if store.is_some_and(|s| {
s.0.unit_created_by().is_some_and(|g| g != 0)
&& s.0.unit_summoned_by().is_none_or(|g| g == 0)
&& s.0.unit_flags() & 0x200 != 0
}) {
continue;
}
// **The enemy/friendly split is `CanAttack`, run PLAYER → UNIT** — not a reaction-rank
// threshold, which is what this shipped with and what 1530 corrects. The two halves of a
// plate genuinely run the reaction in opposite directions: the CATEGORY here asks "can I
// attack it?" (and for a rep-slot faction that question is answered by the AT-WAR bit,
// never by the standing), while the bar COLOUR below asks the unit for its reaction toward
// us — where the standing IS the input. A not-at-war neutral-standing NPC is therefore a
// friendly-category plate with a yellow bar, exactly as the reference draws it.
let rank = ring_reaction(
world.factions.as_deref(),
&world.reputations,
store,
self_store,
);
let is_player = net.kind == EntityKind::Player;
let friendly = crate::target::ring::plate_is_friendly(
world.factions.as_deref(),
&world.reputations,
store,
self_store,
is_player,
);
if friendly && !mode.friends || !friendly && !mode.enemies {
continue;
}
// The gate's dead-looking leg, which sits on the HOSTILE side of that same split
// (`0x60f72f test al,al; jne` → friendly goes to the creature-type check `0x60f750`;
// hostile falls into `0x60f733 shr eax,5; test al,1` → hide). So a feigning hunter's
// plate vanishes for the enemies he is hiding from, which is the point of the spell,
// while a friendly one keeps his (decision 1022).
if !friendly && store.is_some_and(|s| s.0.unit_reads_dead()) {
continue;
}
// Project the plate point: the overhead anchor + 2/3 yd, per frame, no smoothing — and
// honour the projector's ACCEPT verdict ([`crate::ui_pass::project_overlay`]): behind the
// camera or outside the viewport, there is no plate. That is the reference's own law, and
// a hard one — `0x60f600` **destroys** the plate frame on a false verdict, before it ever
// reaches the seat (wow-re `nameplate-offscreen-cull.md`, §5-VERIFIED 2026-08-15).
//
// Ours instead ran an off-screen point into the seat, whose clamp translates a rect from
// anywhere bodily onto the screen: **30% of every drawn plate** was a unit nobody could
// see, pinned to a border, claiming bucket space and shoving the plates of units you can
// (1341 — two thirds of them while standing still, and 90% of the reported jitter).
let anchor = overhead_anchor(
entity,
tf,
&anchor_q.0,
&anchor_q.1,
&anchor_q.2,
&anchor_q.3,
&anchor_q.4,
);
let Some(screen) =
crate::ui_pass::project_overlay(cam, &cam_tf, anchor + Vec3::Y * PLATE_LIFT, viewport)
else {
continue;
};
cands.push((
screen.distance_squared(sort_pt),
screen,
anchor,
rank,
is_player,
entity,
guid,
store,
));
}
cands.sort_by(|a, b| a.0.total_cmp(&b.0));
for (_, screen, anchor, rank, is_player, entity, guid, store) in cands {
plates.0.insert(entity);
// The target-highlight relative alpha: target (or nobody targeted) opaque, others 0x7F.
let alpha = if !has_target || world.selection.target == Some(entity) {
1.0
} else {
DIM_ALPHA
};
// The bar tint: the NAMEPLATE palette in `0x7cbaa0`'s exact order.
let tint = plate_tint(rank, is_player);
// The frame SEAT (§8 Q5, byte-confirmed): the plate's TOP-CENTER lands on the projected
// point — the plate HANGS BELOW head + 2/3 yd — and the point is edge-clamped half a
// plate inside every screen border (`SetPoint(TOP ← root.BOTTOMLEFT, clampedX/Y)`).
// The highlight is a 2-D hover over THIS rect (the frame's OnEnter — yellow name). `pw`/`ph`
// (the plate's logical size) are hoisted above the loop — the border resample keys off them.
// Geometry trace for the vplates capture (`WOW_VPLATE_TRACE=1`): the exact plate rects
// this frame, in logical px — the machine-side check the capture PNG can't give
// (fill/border/text hues overlap under zoom).
let trace = std::env::var("WOW_VPLATE_TRACE").as_deref() == Ok("1");
// The full seat (`0x509ec0`): the desired rect TOP-anchored on the raw projected point
// (the plate hangs below head + 2/3 yd), then the bucket-0 seat law — normalize, SOLVE
// off the plates already claimed this frame, clamp the resolved center-X/top half a
// plate inside every border, rebuild ([`crate::smart_rect`]). The old bare edge-clamp
// was this law minus the solve (nameplate-vkey §8.5, now corrected by the solver note).
let desired = Rect::new(
screen.x - pw * 0.5,
screen.y,
screen.x + pw * 0.5,
screen.y + ph,
);
let solved = bucket.resolve(desired, viewport);
// The rect snaps to the device pixel grid ([`device_snap`]) — the LEFT edge, not the
// centre: an odd width would put a snapped centre's edges back between texels. The CLAIM
// takes the snapped rect, so later plates dodge exactly what is drawn.
let top = device_snap(solved.min.y, scale);
let left = device_snap(solved.min.x, scale);
let plate = Rect::new(left, top, left + pw, top + ph);
// **The jitter decomposition** (`WOW_MOVE_TRACE` tag `vpl`, one line per plate per frame):
// the world anchor, the camera pose that projected it, the raw projected point, the
// solver's answer, and the snapped rect — so "the plates are jittery when moving" is
// attributed to one of the four (a moving anchor, a noisy camera, a flipping solve, the
// pixel snap) from numbers rather than from the eye. It rides the shared trace — tag-
// filtered, one line per plate — and not the `WOW_VPLATE_TRACE` eprintlns beside it, whose
// four unbuffered writes per plate per frame would distort the frame pacing the question
// is about (the 0880 lesson).
if benilla_assets::trace::enabled_for("vpl") {
let (cp, cf) = (cam_pose.translation, cam_pose.forward());
benilla_assets::trace::line(
"vpl",
&format!(
"e={} vp=({:.0},{:.0}) anchor=[{:.4},{:.4},{:.4}] cam=[{:.4},{:.4},{:.4}] \
fwd=[{:.4},{:.4},{:.4}] scr=({:.3},{:.3}) solved=({:.3},{:.3}) plate=({:.1},{:.1})",
entity.index(),
viewport.x,
viewport.y,
anchor.x,
anchor.y,
anchor.z,
cp.x,
cp.y,
cp.z,
cf.x,
cf.y,
cf.z,
screen.x,
screen.y,
solved.min.x,
solved.min.y,
plate.min.x,
plate.min.y,
),
);
}
bucket.claim(plate);
// The plate's own hover, from last frame's layout: the widget layer answers which unit's
// plate the pointer is inside, and the driver resolves it to this frame's entity below.
let hover = hovered_key == Some(guid.0);
// The highlight trigger — the watcher's OR over the two globals: this unit is the
// MOUSEOVER (the 3-D body pick, or the plate's own hover — [`PlateHover`], which the
// widget layer answered above) or the current TARGET. `hover` joins in directly so the
// bar brighten never lags the yellow name by the frame the mouseover takes to publish.
let lit =
hover || world.hovered.target == Some(entity) || world.selection.target == Some(entity);
if trace {
eprintln!(
"vplate-trace: viewport={viewport:?} screen=({:.2},{:.2}) plate=({:.1},{:.1})..({:.1},{:.1}) lit={lit}",
screen.x, screen.y, plate.min.x, plate.min.y, plate.max.x, plate.max.y
);
}
// ── The plate's state, handed to the widget layer ─────────────────────────────────
//
// Everything above is unchanged: the gate, the anchor, the projection's ACCEPT verdict,
// the distance-sorted seat and the bucket solve are all about the WORLD, and they stay
// the app's. What changed in decision 2148 is only what happens with the answer — it used
// to become six quads, and it now becomes one [`PlateState`] for a real widget, which the
// shared frame→quad path draws and an addon can read, hook, restyle or take over.
//
// The seam scale (`windowH/768 × uiScale`) converts our window px into the FrameXML units
// the widget layer speaks. Dividing it out here is deliberate and is a DIVERGENCE worth
// naming: the reference's plates are outside the `uiScale` cascade (uiScale is
// `UIParent`'s own frame scale there, and the WorldFrame is a sibling root), while
// benilla folds uiScale into one global seam (0582/0584). Until that seam grows a
// per-cascade answer, the driver compensates, so a plate's PIXELS stay uiScale-blind the
// way the reference's are.
// The raid-target board (decision 0434 §6): 1-based here, 0-based for the atlas cell.
let mark = group.raid_target_index(guid.0);
let x_units = (plate.min.x + plate.max.x) * 0.5 / seam;
let y_units = (viewport.y - plate.min.y) / seam;
if hovered_key == Some(guid.0) {
plate_hover.0 = Some(entity);
}
for click in &clicked {
if click.key == guid.0 {
match click.button.as_str() {
"RightButton" => plate_clicks.right.push(entity),
_ => plate_clicks.left.push(entity),
}
}
}
states.push(PlateState {
// The plate's identity is the UNIT's, for the life of the plate — the reference's
// `[unit+0xe60]` binding, which every addon's per-plate cache rests on.
key: guid.0,
top_centre: (x_units, y_units),
// RAW health and its max — `healthbar:GetValue()` is `[bar+0x320]`, not a fraction
// (`nameplate-lua-surface.md` Q5, which corrected wow-re's own note). pfUI and
// CustomNameplates both divide, and a fraction here would read as full health.
health: store.and_then(|s| s.0.unit_health()).unwrap_or(0) as f32,
max_health: store
.and_then(|s| s.0.unit_max_health())
.unwrap_or(1)
.max(1) as f32,
bar_colour: [tint[0], tint[1], tint[2]],
name: names
.resolve(guid.0, &net_commands)
.map(str::to_owned)
.unwrap_or_default(),
// The skull's two legs (`0x7cbb40`, §7-VERIFIED): a WORLD BOSS — creature-
// classification rank 3, through the client's own getter (`gated_rank`, decision
// 0782, which is why a MIND-CONTROLLED boss shows its number) — unconditionally; or a
// hostile ≥ 10 levels up that isn't trivial-grey (`0x5f0700`, the shared check,
// vacuous on a ≥ +10 hostile and kept for transcription fidelity).
//
// **A unit with no level yet is not a boss.** The two facts travel separately, because
// folding them into one `Option` made a missing `UNIT_FIELD_LEVEL` read as a skull —
// the old painter kept the whole block inside `if let Some(level)`, so no level meant
// no number AND no skull, and that is the behaviour restored here.
level: store.and_then(|s| s.0.unit_level()),
skull: store.and_then(|s| s.0.unit_level()).is_some_and(|level| {
crate::names::gated_rank(
benilla_protocol::guid::entry(guid.0).and_then(|e| names.creature_record(e)),
store,
) == 3
|| (rank <= 1 && level >= my_level + 10 && !unit_is_grey(my_level, level))
}),
level_colour: {
let c = store
.and_then(|s| s.0.unit_level())
.map_or([1.0, 1.0, 1.0, 1.0], |level| con_color(my_level, level));
[c[0], c[1], c[2]]
},
// The 0434 §6 board, 0-based for the atlas (`col = idx & 3`, `row = idx >> 2`).
raid_icon: (mark >= 1).then(|| mark - 1),
alpha,
lit,
hovered: hover,
});
}
// The window-derived half, once per frame — the widget layer rewrites the static anchors only
// when this actually moves, which is what keeps an addon's own re-anchoring alive between
// resizes (decision 2148 §5).
script.sync_nameplates(
PlateGeometry {
width: pw / seam,
height: ph / seam,
bar_off_x: gx(BAR_OFF_X) / seam,
bar_off_y: gx(BAR_OFF_Y) / seam,
bar_width: gx(BAR_W) / seam,
bar_height: gx(BAR_H) / seam,
level_off_x: gx(LEVEL_OFF_X) / seam,
level_off_y: gx(LEVEL_OFF_Y) / seam,
skull_size: gx(SKULL_SIZE) / seam,
raid_size: gx(RAID_ICON_SIZE) / seam,
name_height: text_px(NAME_H, basis) / seam,
level_height: text_px(LEVEL_H, basis) / seam,
// A constant ONE LOGICAL PIXEL of drop shadow, in the widget layer's units — the
// director's 2026-07-07 tuning, which the shared text arm would otherwise scale by
// this same seam and draw 2 px thick from a 1152-tall window up.
shadow_offset: 1.0 / seam,
},
&states,
);
}
/// The set the plate drive runs in — the overhead-name driver orders after it (the
/// ShouldShowName exclusivity: a plated unit draws no floating name).
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct VPlateSet;
/// V-key nameplates: the toggles + the per-frame gate/draw.
pub(crate) struct VPlatesPlugin;
impl Plugin for VPlatesPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<VPlateMode>()
.init_resource::<VPlates>()
.init_resource::<PlateHover>()
.init_resource::<PlateClicks>()
.add_systems(
Update,
(
toggle_vplates,
// After the targeting chain (selection/hover verdicts) — which is
// `.after(WorldStage::Input)`, so the camera this projects through is THIS
// frame's, the freshest data a plate can be built from.
//
// **And the paint is this frame's too, since decision 2168.** The UI pass is
// two systems: the tick/resolve half stays ahead of `WorldStage::Input`
// (the hit test feeds `PointerOverUi`, which the camera reads), and the QUAD
// half — `ui_script::extract::paint_script` — runs after this driver. 2148
// shipped with the whole pass ahead of the camera, so the anchors written here
// were drawn by the NEXT frame's extract: the seat reached the paint 16 ms
// late, every frame, which is the director's "way more jittered when the
// creature is moving" (measured both ways on the `vpl` trace, 2026-09-10 —
// median driver→paint gap 16.0 ms before, 0.0 ms after).
drive_vplates.after(TargetUpdate),
)
.chain()
.in_set(VPlateSet),
)
// **Outside [`VPlateSet`] deliberately.** Its only ordering need is to be ahead of the
// script tick, and three sets order *after* `VPlateSet` while `drive_vplates` runs
// after the targeting chain — pulling the whole set in front of `UiInput` to carry one
// system would rewire all of that. A V press can therefore reach the globals a frame late,
// which costs nothing: their only readers are `UpdateNameplates` at the two world-entry
// events and an addon that calls it, never a per-frame path.
.add_systems(Update, feed_plate_globals.before(crate::ui_script::UiInput));
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The client's own con law (§7): thresholds at level 30 (grayband[6] = 7 → green down to
/// 23, gray at 22), the exact softened dwords, and the low-level edge (grayband[0] = 4, so
/// a level-1 never grays anything).
#[test]
fn con_color_matches_the_byte_table() {
assert_eq!(con_color(30, 35), CON_RED);
assert_eq!(con_color(30, 33), CON_ORANGE);
assert_eq!(con_color(30, 28), CON_YELLOW);
assert_eq!(
con_color(30, 23),
CON_GREEN,
"gap 7 = grayband[6], still green"
);
assert_eq!(con_color(30, 22), CON_GRAY, "gap 8 crosses the band");
assert_eq!(con_color(1, 1), CON_YELLOW);
assert_ne!(
con_color(5, 1),
CON_GRAY,
"grayband[1] = 4 ≥ the max gap at 5"
);
// The exact dwords (0xFFFF1919 / 0xFFFF7F3F / 0xFF3FB23F): byte-pinned, not the
// FrameXML QuestDifficultyColor approximations.
assert_eq!(CON_RED[1], 25.0 / 255.0);
assert_eq!(CON_ORANGE[2], 63.0 / 255.0);
assert_eq!(CON_GREEN[1], 178.0 / 255.0);
}
/// The snap is on the DEVICE grid, not the logical one. At the 2× scale we develop on, a
/// logical `round()` moved the plate two physical pixels at a time; this moves it one, which
/// is the smallest step that still lands the border blit on a texel boundary. Pinned so the
/// `scale` argument can't be "simplified" away back into `round()`.
#[test]
fn the_plate_snaps_on_the_device_grid_not_the_logical_one() {
// 2× display: the grid is every half logical pixel, and every snapped value is a whole
// number of PHYSICAL pixels.
for (v, want) in [(10.0, 10.0), (10.2, 10.0), (10.3, 10.5), (10.6, 10.5)] {
let got = device_snap(v, 2.0);
assert_eq!(got, want, "{v} at 2×");
assert_eq!((got * 2.0).fract(), 0.0, "{got} is a whole physical pixel");
}
// 1×: unchanged from the old law.
assert_eq!(device_snap(10.4, 1.0), 10.0);
assert_eq!(device_snap(10.6, 1.0), 11.0);
// 1.5× (the Windows norm), where a logical round() was never texel-aligned at all.
assert_eq!((device_snap(10.4, 1.5) * 1.5).fract(), 0.0);
assert_eq!((device_snap(10.9, 1.5) * 1.5).fract(), 0.0);
}
/// Plate text ems ride the gx DIAGONAL like the frame geometry, growth-damped past the
/// director's [`PLATE_DIAG_KNEE`] — name em 13 at the reference's 1152×648 window (measured),
/// level em 11 (director-pinned one under the byte 0.009); at larger windows the em lands midway
/// between the byte law and the 0185 native pin (2560×1440: byte law 29, pin 13 → ours 21). The
/// raw em still carries the client's 32 atlas-cell cap.
#[test]
fn plate_text_sizes_take_the_damped_diagonal_basis() {
let ref43 = plate_basis(Vec2::new(1024.0, 768.0));
let refwin = plate_basis(Vec2::new(1152.0, 648.0));
assert_eq!(text_px(NAME_H, ref43), 13.0);
assert_eq!(text_px(LEVEL_H, ref43), 11.0);
assert_eq!(text_px(NAME_H, refwin), 13.0);
assert_eq!(text_px(LEVEL_H, refwin), 11.0);
let big = plate_basis(Vec2::new(2560.0, 1440.0));
assert_eq!(text_px(NAME_H, big), 21.0, "midway between 29 and 13");
assert_eq!(text_px(NAME_H, 10_000.0), 32.0, "atlas-cell cap, raw law");
}
/// **A key press moves the CVar too** — the half a settings page cannot see for itself. V
/// flips the resource (the bitmask) AND mirrors into the table as an ENGINE write, which is
/// what dirties `config.toml` and what the Nameplates page's checkbox reads next time it
/// opens. Without the mirror, plates toggled by key would silently revert at every launch and
/// the window would show the wrong state.
///
/// The table is seeded at `"0"/"0"` because that is what the app seeds it with: both plate
/// CVars boot at the reference's OFF since 1804 ([`VPlateMode::default`]), so the first V of a
/// session turns plates ON. The mirror is direction-blind, and the second press below is here
/// to say so.
#[test]
fn the_v_key_mirrors_into_the_cvar_table() {
use crate::bindings::{cmd, BindingsState};
let mut app = App::new();
let script = benilla_ui::script::UiScript::new().unwrap();
script.register_cvars([(CVAR_ENEMIES, "0"), (CVAR_FRIENDS, "0")]);
app.add_systems(Update, toggle_vplates)
.init_resource::<VPlateMode>()
.insert_non_send_resource(script)
.insert_resource(BindingsState::test_fired(&[cmd::NAMEPLATES]));
app.update();
assert!(app.world().resource::<VPlateMode>().enemies, "V turns on");
let mut script = app
.world_mut()
.non_send_resource_mut::<benilla_ui::script::UiScript>();
assert_eq!(
script.take_cvar_changes(),
vec![(CVAR_ENEMIES.to_string(), "1".to_string())],
"the change queues, so the host dirties the config file"
);
assert_eq!(script.cvar(CVAR_FRIENDS).as_deref(), Some("0"), "untouched");
// And back: the same key mirrors the OFF as an engine write too.
app.world_mut()
.insert_resource(BindingsState::test_fired(&[cmd::NAMEPLATES]));
app.update();
assert!(!app.world().resource::<VPlateMode>().enemies, "V turns off");
assert_eq!(
app.world_mut()
.non_send_resource_mut::<benilla_ui::script::UiScript>()
.take_cvar_changes(),
vec![(CVAR_ENEMIES.to_string(), "0".to_string())]
);
// Shift-V is the other bit, and only the other bit.
app.world_mut()
.insert_resource(BindingsState::test_fired(&[cmd::FRIEND_NAMEPLATES]));
app.update();
assert!(app.world().resource::<VPlateMode>().friends);
assert_eq!(
app.world_mut()
.non_send_resource_mut::<benilla_ui::script::UiScript>()
.take_cvar_changes(),
vec![(CVAR_FRIENDS.to_string(), "1".to_string())]
);
// A frame with nothing fired writes nothing at all.
app.world_mut().insert_resource(BindingsState::default());
app.update();
assert!(app
.world_mut()
.non_send_resource_mut::<benilla_ui::script::UiScript>()
.take_cvar_changes()
.is_empty());
}
/// **The FrameXML mirror is owed to every VM** (decision 2132) — the mode's two bits reach
/// `NAMEPLATES_ON`/`FRIENDNAMEPLATES_ON` as the reference's `1`-or-nil, follow a change, and
/// are handed to a rebuilt VM (a `/reload`) without one.
#[test]
fn the_plate_globals_follow_the_mode_and_survive_a_rebuilt_vm() {
let read = |app: &mut App| {
let s = app
.world_mut()
.non_send_resource_mut::<benilla_ui::script::UiScript>();
(
s.lua().globals().get::<Option<i64>>("NAMEPLATES_ON").ok(),
s.lua()
.globals()
.get::<Option<i64>>("FRIENDNAMEPLATES_ON")
.ok(),
)
};
let mut app = App::new();
app.add_systems(Update, feed_plate_globals)
.init_resource::<VPlateMode>()
.insert_non_send_resource(benilla_ui::script::UiScript::new().unwrap());
app.update();
assert_eq!(read(&mut app), (Some(None), Some(None)), "both off ⇒ nil");
app.world_mut().resource_mut::<VPlateMode>().enemies = true;
app.update();
assert_eq!(
read(&mut app),
(Some(Some(1)), Some(None)),
"the reference's own truthiness: the NUMBER 1, never a truthy `0`"
);
// `ReloadUI()`: a fresh VM, and the mode did not move. The memo is keyed on the VM's
// session (1290), so the new one is told again rather than inheriting the old one's claim.
app.insert_non_send_resource(benilla_ui::script::UiScript::new().unwrap());
assert_eq!(
read(&mut app),
(Some(None), Some(None)),
"a fresh VM knows nothing"
);
app.update();
assert_eq!(
read(&mut app),
(Some(Some(1)), Some(None)),
"…and is handed the mode again"
);
}
/// The palette selector follows `0x7cbaa0`'s exact test order — notably reaction 2
/// (unfriendly) is YELLOW with 3, not hostile red, and a hostile player is red before the
/// player-blue test.
#[test]
fn plate_tint_matches_the_byte_order() {
assert_eq!(plate_tint(1, false), PLATE_HOSTILE);
assert_eq!(plate_tint(1, true), PLATE_HOSTILE, "hostile beats player");
assert_eq!(plate_tint(2, false), PLATE_NEUTRAL, "unfriendly is yellow");
assert_eq!(plate_tint(3, false), PLATE_NEUTRAL);
assert_eq!(plate_tint(3, true), PLATE_PLAYER, "player beats neutral");
assert_eq!(plate_tint(4, false), PLATE_FRIENDLY);
assert_eq!(plate_tint(6, true), PLATE_PLAYER, "player beats friendly");
}
/// The plate geometry law through the gx DIAGONAL unit, growth-damped past
/// [`PLATE_DIAG_KNEE`]: at 1024×768 (diag exactly 1280) the 0.1 × 0.025 frame = 128 × 32 px
/// — the border texture's native size, the bar ≈ 103 × 9 px. Past the knee the frame lands
/// midway between the byte law and native (1080p: byte law 220, native 128 → ours 174).
/// Below the knee the diagonal rules unchanged (a smaller window shrinks the plate, §8 Q4).
#[test]
fn plate_geometry_damps_past_the_native_knee() {
let ref43 = plate_basis(Vec2::new(1024.0, 768.0));
assert_eq!(gx_px(PLATE_W, ref43), 128.0);
assert_eq!(gx_px(PLATE_H, ref43), 32.0);
assert_eq!(gx_px(BAR_W, ref43), 103.0);
assert_eq!(gx_px(BAR_H, ref43), 9.0);
assert_eq!(
gx_px(PLATE_W, plate_basis(Vec2::new(1920.0, 1080.0))),
174.0,
"midway between 220 and 128"
);
assert_eq!(gx_px(PLATE_W, plate_basis(Vec2::new(800.0, 600.0))), 100.0);
}
}