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:
@@ -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));
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user