- 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>
174 lines
7.2 KiB
Rust
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"
|
|
);
|
|
}
|