your own name survives the first login, and four cold pipelines warm

- your character's own name no longer goes missing on the first login of a fresh start, so KLHThreatMeter stops erroring: the four "player" verbs read the login record, as the reference does (2260, 2261, 2263)
- four pipelines that compiled live mid-play now warm under the loading screen: the model tile atlas's orthographic view, the minimap composite's quad, the glow combine pair and the indoor pool's liquid layout (2262, 2264)
- a hoverable sign seats its tooltip where its template says, at the cursor or in the corner, not by its object type: Brill's signpost still follows the cursor (2259)
- three warnings that fired when nothing was wrong are quiet, and the lane-coverage gates walk every crate instead of one (2264)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-17 16:37:17 +07:00
co-authored by Claude Fable 5.1
parent e0f752eb73
commit cbe8c8557c
25 changed files with 1426 additions and 96 deletions
@@ -534,7 +534,11 @@ fn the_item_button_helpers_paint_a_slots_icon_and_count() {
fn the_roster_seat_names_the_character_the_addons_will_meet() {
let seat = super::seat_from_roster(&roster()).expect("a pending pick seats a player");
assert_eq!(seat.name.as_deref(), Some("Harness"));
assert_eq!(seat.level, 60);
// **0, not 60 — the reference's answer, byte-verified** (decision 2263). `UnitLevel 0x517fc0`
// carries no `"player"` fast path at all: it resolves the token, misses (no object, no roster
// record for a zero GUID) and reaches `0x51813e push 0; push 0` — the NUMBER 0, one return.
// The roster's real level arrives with the descriptor, within the second.
assert_eq!(seat.level, 0);
assert_eq!(seat.race_file.as_deref(), Some("Human"));
assert_eq!(seat.class_file.as_deref(), Some("WARRIOR"));
assert_eq!(seat.sex, 2, "the wire's 0 is UnitSex's 2");
@@ -543,7 +547,17 @@ fn the_roster_seat_names_the_character_the_addons_will_meet() {
Some("Alliance"),
"nil here is 24 corpus addons stopping on AceDB-2.0's file-scope concatenation"
);
assert!(seat.exists && seat.is_player);
// **`exists` is false, and that is the correction 2263 made** (it was `true` from 1230 until
// then). `UnitExists 0x515fb0` has no fast path either; its resolver reads the GUID out of the
// OBJECT (`0x515994`), so with none it holds `0:0`, and the roster fallback `0x491900` bails
// on a zero GUID at `0x4e80aa je` BEFORE fetching the active player — so the `0 == 0` that
// would answer "that's me" never runs, and `0x516001` pushes nil.
//
// Safe to correct only *because* of the record above: 1230 seated a whole unit in order to
// deliver a name at addon file scope, and the name no longer needs a unit to exist. The rest
// of the seat is untouched and still load-bearing — the faction side below above all.
assert!(!seat.exists);
assert!(seat.is_player);
// Deliberately NOT invented — the descriptor says these, within the second.
assert_eq!((seat.health, seat.max_health), (0, 0));
+71 -7
View File
@@ -502,6 +502,21 @@ pub(crate) fn load_ingame_ui_on_world_entry(world: &mut World) {
script.set_realm_name(&realm);
// …and so does the PLAYER, for the same reason and with more riding on it — see
// [`seat_from_roster`], which is where the why lives.
//
// **The player RECORD first, and separately** (decisions 2261/2263). The reference keeps the
// local player's name, race, class and gender somewhere no cache, feed or object can reach — a
// copy of the char-enum row written at the Enter World commit (`0x5abd9e`) and never cleared —
// and `UnitName`/`UnitRace`/`UnitClass`/`UnitSex` read only that, unconditionally, even after
// the descriptor exists. The snapshot below is the descriptor's stand-in until the real one
// streams in and is replaced when it does; the record outlives every push. Seeded here because
// our VM is rebuilt per login (1290) while the reference's record simply persists, so each new
// VM has to be told once — before the addon walk, like the realm.
if let Some(record) = world
.get_resource::<crate::char_select::Roster>()
.and_then(record_from_roster)
{
script.set_player_record(record);
}
if let Some(seat) = world
.get_resource::<crate::char_select::Roster>()
.and_then(seat_from_roster)
@@ -660,6 +675,43 @@ pub(crate) fn load_ingame_ui_on_world_entry(world: &mut World) {
}
}
/// The wire's gender byte (0 male, 1 female) on `UnitSex`'s own 2/3 scale — the one mapping, so
/// the record and the snapshot can never disagree about which is which.
fn roster_sex(gender: u8) -> u8 {
match gender {
0 => 2,
1 => 3,
_ => 0,
}
}
/// **The local player record the UI loads under** — our copy of the char-enum row the reference
/// copies at the Enter World commit (decision 2263; the bytes are on
/// [`benilla_ui::script::PlayerRecord`]).
///
/// The same roster row [`seat_from_roster`] builds the `"player"` *snapshot* from, and
/// deliberately the same `race_names`/`class_names` lookups: the reference resolves the record's
/// race and class bytes through `ChrRaces`/`ChrClasses` at call time, so resolving them once here
/// is the same answer, and sharing the lookup is what stops the record and the snapshot drifting
/// into two opinions about what race 4 is.
///
/// **The level is not here, and that is byte-verified, not an omission** — the reference's record
/// carries a level at `+0x108` and ships an accessor for it (`0x5abe00`) that *nothing calls*, so
/// `UnitLevel("player")` reads the descriptor.
pub(crate) fn record_from_roster(
roster: &crate::char_select::Roster,
) -> Option<benilla_ui::script::PlayerRecord> {
let row = roster.pending_row()?;
let race = crate::ui_unit::race_names(row.race);
let class = crate::ui_unit::class_names(row.class);
Some(benilla_ui::script::PlayerRecord {
name: row.name.clone(),
race: race.map(|(n, f)| (n.to_string(), f.to_string())),
class: class.map(|(n, f)| (n.to_string(), f.to_string())),
sex: roster_sex(row.gender),
})
}
/// The `"player"` snapshot the UI loads **under**, built from the roster row of the pick in
/// flight — `None` when there is no pick (a capture, a scenario, a test world).
///
@@ -693,20 +745,32 @@ pub(crate) fn seat_from_roster(
let row = roster.pending_row()?;
let race = crate::ui_unit::race_names(row.race);
let class = crate::ui_unit::class_names(row.class);
let sex = roster_sex(row.gender);
Some(benilla_ui::script::UnitState {
exists: true,
// **`exists` is FALSE, and the level is 0 — the reference's answers, byte-verified**
// (decision 2263). `UnitExists("player")` `0x515fb0` has no fast path: it resolves the
// token, and the resolver reads the GUID out of the OBJECT (`0x515994`), not out of
// `[mgr+0xc0]` — so with no object it holds `0:0`. The roster fallback `0x491900` then
// bails on a zero GUID at `0x4e80aa je` *before* it fetches the active player, so the
// `0 == 0` that would otherwise answer "yes, that's me" is never reached: `0x516001
// lua_pushnil`. `UnitLevel 0x517fc0` likewise carries no `"player"` compare at all and
// reaches its total-miss arm `0x51813e push 0; push 0` — the **number 0**, not nil. The
// two misses are deliberately asymmetric and a client that modelled "no data" uniformly
// would be wrong on one of them (wow-re `ui/scratch/unit-verbs-before-player-object.md`
// §2-§3; it also corrected that repo's own `main`, which had published the opposite).
//
// This seat is no longer what makes the player *answerable* — the record above is — so it
// no longer has to claim a unit exists in order to deliver a name. What it still carries
// is the handful of fields the corpus reads at file scope that are neither the record's
// nor the descriptor's to say yet, the faction side below chief among them.
exists: false,
name: Some(row.name.clone()),
level: u32::from(row.level),
race: race.map(|(n, _)| n.to_string()),
race_file: race.map(|(_, f)| f.to_string()),
class: class.map(|(n, _)| n.to_string()),
class_file: class.map(|(_, f)| f.to_string()),
// The wire's 0/1 on `UnitSex`'s 2/3 scale — `ui_unit::snapshot`'s own mapping.
sex: match row.gender {
0 => 2,
1 => 3,
_ => 0,
},
sex,
is_player: true,
player_controlled: true,
// Nil here is not "no faction", it is a state a player character cannot be in, and
@@ -565,3 +565,44 @@ fn a_cursor_seated_gameobject_plate_survives_an_addons_on_show_hook() {
s.errors()
);
}
/// **The CORNER arm must end up owned too — the other half of the existence gate** (decision 2259).
///
/// The reference's corner arm (`0x492a42`) writes no owner itself; the owner is restored purely by
/// the `+0x444` handler, `OnTooltipSetDefaultAnchor` → `GameTooltip_SetDefaultAnchor(this,
/// UIParent)` → `SetOwner(UIParent, "ANCHOR_NONE")`. And the owner really is 0 on the way in:
/// `Tooltip::Hide 0x530a60` *is* `SetOwner(NULL, 0, 0, 0)`, so every hover starts un-owned.
///
/// That makes this test the precondition for narrowing the placement fork at all. Moving an object
/// from the cursor arm to the corner arm is only safe while the corner arm produces an OWNED,
/// SHOWN plate — otherwise those objects would build their lines and then hide, which is precisely
/// the failure 2255 had just fixed on the cursor arm.
#[test]
fn a_corner_seated_gameobject_plate_is_owned_and_shown() {
let mut s = harness(&[]);
assert!(s.world_tooltip_gameobject("Ironforge Main Gate", &[], None));
let owned: bool = s.eval("return GameTooltip:IsOwned(UIParent)").unwrap();
assert!(owned, "the corner plate is owned; errors: {:?}", s.errors());
let shown: bool = s
.eval("return GameTooltip:IsShown() and true or false")
.unwrap();
assert!(shown, "and it is on screen; errors: {:?}", s.errors());
}
/// And it survives the same addon hook the cursor arm had to: `!Questie`'s `OnShow` handler ends in
/// `GameTooltip:Show()`, and `:Show()` is the existence gate `0x530a80`.
#[test]
fn a_corner_seated_gameobject_plate_survives_an_addons_on_show_hook() {
let mut s = harness(&[]);
s.run(r#"GameTooltip:SetScript("OnShow", function() GameTooltip:Show() end)"#)
.unwrap();
assert!(s.world_tooltip_gameobject("Ironforge Main Gate", &[], None));
let shown: bool = s
.eval("return GameTooltip:IsShown() and true or false")
.unwrap();
assert!(
shown,
"the corner plate is still up; errors: {:?}",
s.errors()
);
}
@@ -1657,3 +1657,68 @@ ScreenProbeHeight = GetScreenHeight()
drop(world);
let _ = std::fs::remove_dir_all(&tmp);
}
/// **The entry load seeds the player record, not just the snapshot** (decisions 2261/2263).
///
/// [`super::seat_from_roster`]'s `"player"` push is the descriptor's stand-in and is *replaced*
/// the moment the real one streams in — which is how decision 2260's nameless push reached
/// `UnitName("player")`. The buffer is seeded beside it, from the same roster row, and the verb
/// reads only that; so the token can be replaced by a nameless snapshot or removed outright and
/// the name still answers, exactly as the reference's never-cleared `0xc27d88` does.
#[test]
fn the_entry_load_seeds_a_record_the_feed_cannot_take_away() {
let _l = ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let (tmp, _capture, _home) = hermetic_probe("nameseed");
let mut world = booted_world();
log_in_as(&mut world, "Nelprifour", 0x2A);
assert_eq!(
probe_saw(&world).as_deref(),
Some("Nelprifour"),
"addon file scope reads the live character, as it always has (1230)"
);
let mut script = world
.get_non_send_resource_mut::<benilla_ui::script::UiScript>()
.expect("a VM");
// The feed's 2260 push: the descriptor landed, the name cache missed for our own guid.
script.set_unit(
"player",
Some(benilla_ui::script::UnitState {
exists: true,
has_object: true,
name: None,
..Default::default()
}),
);
assert_eq!(
script
.eval::<Option<String>>(r#"return UnitName("player")"#)
.unwrap()
.as_deref(),
Some("Nelprifour"),
"the record answers, so a nameless snapshot is invisible to the verb"
);
assert_eq!(
script
.eval::<Option<String>>(r#"local _, t = UnitClass("player"); return t"#)
.unwrap()
.as_deref(),
Some("WARRIOR"),
"…and the same for the other three fields the reference reads off that record (2263)"
);
// …and so does the logout despawn, which removes the token altogether.
script.set_unit("player", None);
assert_eq!(
script
.eval::<Option<String>>(r#"return UnitName("player")"#)
.unwrap()
.as_deref(),
Some("Nelprifour")
);
let _ = std::fs::remove_dir_all(&tmp);
}