Files
benilla-octo/crates/benilla-app/src/ui_script/pet_stable_tests.rs
T
SamandClaude Fable 5.1 6b812913f4 the lamppost halo stops stacking, and the stance bar seats on login
- the lamppost halo stops stacking after a short-range teleport: bevy registers `add_visibility_class::<Mesh3d>` as an add hook that pushes the mesh's class id onto the entity's `VisibilityClass` list on every (re)add, nothing dedups the list, the required component survives a `Mesh3d` removal, and `check_visibility` queues an entity once per entry — so 1949's parking, which removes `Mesh3d` after thirty hidden frames and re-adds it the frame the part is shown, drew a part one more time per hide-then-show cycle; opaque parts hide it at the same depth and colour, an additive glow card adds itself to itself (×2 after the first teleport that hid the lamp for half a second, ×3 after the next), and a loading screen despawns the tiles and respawns fresh entities, which is why a far teleport and a relog never showed it; the unpark queues one dedup after the hook, the test registers bevy's own hook and requirement in a bare world and reads 2 entries without it and 1 with, and on the rig, with the away camera facing the lamp past a 177 yd far clip, a build without the dedup read `stacked_parts=278` after one hop back where the landed build reads 0 (2003, 2005); the instruments that excluded the other mechanisms stay — the inspector's glow-card line names the material's additive bit, fade/unlit flags, depth write, texture residency, tag alpha and live scale, `STACKED n` on the card and `stacked_parts=` on the probe line count the duplicated class lists, and screenshot and pick bursts now keep the rig window awake, because an occluded window recaptures one stale drawable and a day of A/Bs had read identical to the decimal
- `perf::blend_check` reads the half of a glow card's look the material cannot: per `Transparent3d` item, the pipeline descriptor's colour blend against the material's additive marker — the marker folds the radial alpha into the colour in the shader and gives the pipeline its pure (ONE, ONE) add, and 0071's wrong-pipeline class was immunised only for opaque batches; mismatches count into the perf pill as a red `blend xN` the frame they happen, ride the probe line as `blend_mismatch=`, and are tracked across frames and named — object, texture, which pipeline, PERSISTING; its own first harvest was the tracker: an additive material realized this frame is extracted, prepared and drawn this frame, and a tracker reading the `Added` event a frame late called every first draw a mismatch, so it runs after `AssetEventSystems` like bevy's own specialization check; and the hook-accumulation sweep states the class so it can be swept: bevy 0.18.1's insert path fires `on_add` only when the component was absent, `on_replace` on every outgoing value, `on_remove` on removal only — a push on `on_add` accumulates across remove→re-add, a free on `on_remove` leaks across an insert-overwrite; every hook in the bevy crates we link (the visibility class on `Mesh3d`/`Mesh2d`/`Sprite`/`Text2d`/`MeshletMesh3d`, the cluster class on every bevy light) and every removal of a hooked component in our tree was read, parking's `Mesh3d` is the only accumulating pair, the UI quad pools reuse entities by insert-over and fire no add hook, and the one asymmetric freer, `PropProbeSlot`, freed its probe slot on `on_remove` — an insert over a live slot leaked it for the session, held off by a remove-then-insert discipline at the one re-seat site; it frees on `on_replace` now (`RigSkin`'s shape), the re-seat is a plain insert, and a test overwrites a live slot and reads it freed (2005)
- the stance bar loads after everything the managed-position pass reads: the stock `ShapeshiftBar_OnLoad` shows the bar, its OnShow runs `UIParent_ManageFramePositions`, and the stock pass indexes `ReputationWatchBar` unguarded — our manifest seated the bar beside the other bar files where the reference's toc has it at l.74, after the reputation frame at l.57, so the pass raised at `UIParent.lua:1618` on every login and never reached the arm that seats the shelf: the red login error and the sliver above Defensive Stance (the shelf's plate at its XML anchors, a few pixels off its seat, healed by any bottom-bar toggle re-running the pass) were one defect from 1988, whose retired copy of the pass had guarded every read — and the three-form case 2000 could not reproduce; the bar sits at the manifest's tail right before `WorldStateFrame.xml`, whose own OnLoad runs the pass and indexes the shelf textures the stance bar declares — the reference's own reason for l.74 before l.89; a manifest test states the law with the pairs read off the pass's body, and `load_default_ui` now folds the VM's handler errors into its failures, because a raise one call below an OnLoad landed only in the VM's error list and shipped through the manifest test, the full-manifest harnesses and the smoke's WARN alike (2001)
- `UnitName` answers `UNKNOWNOBJECT`, never nil, for a resolved unit whose name is still in flight: a hunter casting Call Pet with the stable window closed got `PetStable.lua:129: attempt to concatenate a nil value`, because stock `PetStable_OnEvent` repaints on `UNIT_PET` without asking whether the frame is shown and the paint concatenates `UnitName("pet")` the instant `UnitExists("pet")` is true, while the pet's name is a `CMSG_PET_NAME_QUERY` round trip away; the reference's `UnitName 0x517020` pushes nil in exactly two cases — the `"player"` fast path (`0x517083`) over an empty name buffer, and a token that resolves to GUID 0 (`0x5170c0`) — and every other path ends in a string: `CGUnit_C::GetUnitName 0x609210` looks the pet up in `petnamecache.wdb` by `UNIT_FIELD_PETNUMBER`, re-sends the query on a miss or a stale stamp, and falls to `0x609324`, the GlobalString `UNKNOWNOBJECT` ("Unknown") read through `0x703bf0`, or the literal `"Unknown Being"` at `0x860fa4` when the global is missing or empty; the binding now reads the global out of the VM's `_G` at call time, so a translated `GlobalStrings.lua` translates this too, the stock file is untouched, and 1688's "nil for an unresolved name is faithful" is corrected — the reference paints `Unknown Level 58 Boar` into the hidden frame and the next repaint reads the name; `GameTooltip:SetUnit` still titles a pending-name unit with an empty line, a next move (2002)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 19:32:13 +07:00

174 lines
7.2 KiB
Rust

//! The reference's own stable window (`Interface\FrameXML\PetStable.xml`, off the player's chain
//! since 1751) driven through the one event path that reaches it **while it is closed**:
//! `PetStable_OnLoad` registers `UNIT_PET`, and `PetStable_OnEvent` repaints on it
//! unconditionally (stock `PetStable.lua:20-21`) — so every Call Pet a hunter casts runs the whole
//! of `PetStable_Update` against a pet the client has only just learned about.
//!
//! That paint concatenates `UnitName("pet")` (l.129, and again at l.163-164) the moment
//! `UnitExists("pet")` is true, and the pet's name is the one name the client never has at that
//! moment: it does not ride the descriptor, it is answered by `CMSG_PET_NAME_QUERY` a round-trip
//! later. The reference reads `UNKNOWNOBJECT` there; a verb answering nil raises
//! `attempt to concatenate a nil value` in a window nobody opened (decision 2002).
use benilla_ui::script::{PetStats, ScriptValue, UiScript, UnitState};
use super::test_ui::load_ui_strict;
/// The window's production load prefix, in `benilla.toc` order: `GlobalStrings.lua` for
/// `UNIT_LEVEL_TEMPLATE`/`EMPTY_STABLE_SLOT` (and `UNKNOWNOBJECT`, the string under test),
/// `BasicControls.xml` for `TEXT()`, `ItemButtonTemplate.xml` for `SetItemButtonTexture`,
/// `MoneyFrame` for the cost row, `UIParent.xml` for the slot manager, `BuildListString` and
/// `Model_OnLoad` (the pane is a `<PlayerModel>`), `UIPanelTemplates` for the purchase and close
/// buttons, `GameTooltip.xml` for the slot hovers.
///
/// **Callers must open with `benilla_formats::wow_data_or_skip!()` themselves** — every entry
/// here is a chain entry, and the macro `return`s from the function it is written in.
const STABLE_UI: &[&str] = &[
r"Interface\FrameXML\GlobalStrings.lua",
r"Interface\FrameXML\Fonts.xml",
r"Interface\FrameXML\BasicControls.xml",
r"Interface\FrameXML\ItemButtonTemplate.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
r"Interface\FrameXML\UIParent.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
r"Interface\FrameXML\GameTooltip.xml",
r"Interface\FrameXML\PetStable.xml",
];
fn load_stable() -> UiScript {
let mut s = UiScript::new().unwrap();
s.set_screen_size(1024.0, 768.0);
for f in STABLE_UI {
load_ui_strict(&s, f);
}
s.set_unit(
"player",
Some(UnitState {
exists: true,
name: Some("Benilla".into()),
health: 100,
max_health: 100,
level: 60,
class: Some("Hunter".into()),
class_file: Some("HUNTER".into()),
is_player: true,
player_controlled: true,
..UnitState::default()
}),
);
s
}
/// The pet's snapshot exactly as `ui_pet::unit` pushes it on the tick the summon streams: the
/// descriptor's fields are in, the guid is in, and the name is whatever the name cache has —
/// `None` until `SMSG_PET_NAME_QUERY_RESPONSE` lands.
fn pet(name: Option<&str>) -> UnitState {
UnitState {
exists: true,
has_object: true,
name: name.map(String::from),
health: 900,
max_health: 1000,
level: 58,
guid: 0xF140_0000_0000_0001,
..UnitState::default()
}
}
/// The stat block the pet feed resolves off the creature template the same tick — the icon and
/// the family word come off one `CreatureFamily.dbc` row, which is why `GetPetIcon()` answers
/// (the paint's "is there a current pet?" test, l.51/161) while the NAME is still in flight.
fn hunter_pet_stats() -> PetStats {
PetStats {
icon: Some(r"Interface\Icons\Ability_Hunter_Pet_Boar".into()),
hunter_pet: true,
loyalty: Some("(Loyalty Level 6) Best Friend".into()),
family: Some("Boar".into()),
..PetStats::default()
}
}
fn level_text(s: &UiScript) -> String {
s.eval::<String>("return PetStableLevelText:GetText()")
.unwrap()
}
/// The director's report: a hunter with a pet out casts Call Pet, and the red script-error dialog
/// names `PetStable.lua:129` — with the stable window closed. `UNIT_PET` fires on the summon,
/// the paint runs, `UnitExists("pet")` is true, and the name is not known yet.
///
/// Failure looks like: `s.errors()` holds `PetStable.lua:129: attempt to concatenate a nil
/// value`, and the level line is whatever the XML shipped as placeholder text. The pass is the
/// reference's own line for that instant — `UNKNOWNOBJECT`, the level template, the family —
/// and no error.
#[test]
fn call_pet_repaints_the_closed_stable_with_unknownobject_while_the_name_is_in_flight() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = load_stable();
// No stable master has been visited: nothing pushed through `set_stable`, so every
// `GetStablePetInfo` answers its miss shape and `GetSelectedStablePet()` is -1.
s.set_unit("pet", Some(pet(None)));
s.set_pet_stats(true, hunter_pet_stats());
s.fire_event("UNIT_PET", vec![ScriptValue::Str("player".into())]);
assert!(
s.errors().is_empty(),
"the closed stable window raised on Call Pet: {:?}",
s.errors()
);
assert!(
!s.eval::<bool>("return PetStableFrame:IsVisible()").unwrap(),
"UNIT_PET must repaint the window without opening it"
);
assert_eq!(level_text(&s), "Unknown Level 58 Boar");
assert_eq!(
s.eval::<String>("return PetStableCurrentPet.tooltip")
.unwrap(),
"Unknown",
"the slot's hover title is the same read (l.163)"
);
assert_eq!(
s.eval::<String>("return PetStableCurrentPet.tooltipSubtext")
.unwrap(),
"Level 58 Boar"
);
// The name lands; the next repaint reads it. (`UNIT_NAME_UPDATE` is not one of the window's
// events — a stable-side repaint is what the reference re-reads the name on.)
s.set_unit("pet", Some(pet(Some("Snarl"))));
s.fire_event("PET_STABLE_UPDATE", vec![]);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert_eq!(level_text(&s), "Snarl Level 58 Boar");
assert_eq!(
s.eval::<String>("return PetStableCurrentPet.tooltip")
.unwrap(),
"Snarl"
);
}
/// The control the fix must not move: the dismiss edge. `UNIT_PET` with no pet token seated and
/// no stable row for slot 0 takes the "nothing" arm (l.151-156) — empty level line, no error.
#[test]
fn dismissing_the_pet_empties_the_closed_stable_without_raising() {
let _data = benilla_formats::wow_data_or_skip!();
let mut s = load_stable();
s.set_unit("pet", Some(pet(None)));
s.set_pet_stats(true, hunter_pet_stats());
s.fire_event("UNIT_PET", vec![ScriptValue::Str("player".into())]);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
s.set_unit("pet", None);
s.set_pet_stats(false, PetStats::default());
s.fire_event("UNIT_PET", vec![ScriptValue::Str("player".into())]);
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
assert_eq!(level_text(&s), "");
assert_eq!(
s.eval::<String>("return PetStableCurrentPet.tooltip")
.unwrap(),
"Empty Stable Slot"
);
}