the HUD becomes UIParent's child, and cinematics fade through black
- the transcription had dropped parent= on 72 declarations the reference carries (37 to UIParent, 28 to the action bar's art frame, 7 more), so the HUD was a field of top-level frames and UIParent:Hide() reached almost nothing — which is why a cinematic had to hide the UI at the draw level and then special-case the pointer; both workarounds are deleted and the HUD hides through the reference's own route; restoring only the UIParent half would have been the dangerous part (draw order is one flat sort, so a partial restore lifts frames over the neighbours they carry), and manifest order turns out to be load-bearing — a parent name that doesn't exist yet is silently ignored, so UIParent.xml moves to the reference's own slot with a test that fails if anything declares a child first; the chat still drew over a fly-by after all that, because the reference parents the chat windows on their TEMPLATE, invisible to a name-for-name diff when both sides rename — two declarations fix it, and the sweep that replaces the method loads the whole shipped manifest, fires CINEMATIC_START and asserts the survivors are exactly three: 91 frames visible before, 3 after, and the count comes back on stop - every cinematic boundary cuts through the reference's 0.25 s fade to black, and the next step hangs on the fade's COMPLETION, not a delay — the shot boundary is exactly where the reference does a blocking terrain load, so a delay-driven cut shows through a hitch and a completion-driven one cannot; and a fly-by now ends into black instead of a ~0.3 s loading cover: the reference raises no cover on that path, it holds black while the ground under the player loads back, so ours holds black while the stream catches up — budgeted, never an unbounded wait - the death arc keeps its promises across a relog: the body's own state machine now dies with the session — re-entering the world as a ghost used to match no arm of the world-edge machine, so the corpse query never went out and "Resurrect Now" stayed down until the next death; and a corpse wears its owner's guild crest — the corpse descriptor carries the guild field and resolves through the same crest cache a living tabard does, where ours dressed the body crestless - the char-select ghost gets its body kit: the screen resolves the state kit of the very "Ghost" aura a released ghost rides in the world — tint (140,185,253), alpha 0.5, and the Ghost_state wisp model at the body's attach point, pinned by a golden that decodes the row out of the shipped DBC; the airborne model is corrected before its fix lands — the address six comments called the gravity integrator is really the fall-velocity query, and a knockback is unsteerable because its launch PLANTS the forward bit, not because the arc is airborne (our guard was a velocity proxy, so a near-zero-speed knockback steered from frame 2); and the hover inspect card now decodes a picked part's mesh tag — alpha, rig slot, and both readings of the lighting payload side by side — turning a rebuild-and-two-probes hunt into a hover Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -171,3 +171,58 @@ fn the_screenshot_key_is_handed_back_to_its_binding() {
|
||||
assert!(s.frame_key_input("ESCAPE"));
|
||||
assert!(s.take_keybind_requests().is_empty());
|
||||
}
|
||||
|
||||
/// **The HUD hide, end to end — and the frame that must survive it.**
|
||||
///
|
||||
/// This is the chain decision 1734 restored, and every link was broken until it did:
|
||||
/// `CINEMATIC_START` → `CinematicFrame`'s arm calls `ShowUIPanel` → its `area = "full"` row routes
|
||||
/// to `SetFullScreenFrame` → which hides `UIParent` → which cascades to every frame declaring
|
||||
/// `parent="UIParent"`. Before, `SetFullScreenFrame` had that line dropped, and there was almost
|
||||
/// nothing parented to cascade to; the engine hid the HUD with `UiHidden` instead, and paid for it
|
||||
/// with a mouse that could not find the cinematic frame.
|
||||
///
|
||||
/// It replaces a log line. 1699 verified the takeover by watching `cinematic: HUD hidden for
|
||||
/// playback` go past, because `UiHidden` hides at the *draw* and leaves every widget answering
|
||||
/// `IsVisible() == true` — the predicate was useless. Hiding through the real cascade makes
|
||||
/// `IsVisible` the honest question again, so the check is an assertion instead of a log.
|
||||
#[test]
|
||||
fn a_cinematic_hides_the_hud_through_uiparent_and_spares_the_cinematic_frame() {
|
||||
let mut s = ui_with_the_cinematic_frame();
|
||||
let visible = |s: &UiScript, f: &str| {
|
||||
s.eval::<i64>(&format!("return {f}:IsVisible() and 1 or 0"))
|
||||
.unwrap()
|
||||
== 1
|
||||
};
|
||||
|
||||
// A shown child of UIParent stands in for the HUD: `UiPanels.xml` gives us one without
|
||||
// dragging the whole action bar in, and what is under test is the cascade, not which frame.
|
||||
s.eval::<i64>("StaticPopup1:Show() return 0").unwrap();
|
||||
s.resolve();
|
||||
assert!(visible(&s, "UIParent"), "the HUD's parent starts visible");
|
||||
assert!(visible(&s, "StaticPopup1"), "and so does its child");
|
||||
|
||||
start_cinematic(&mut s);
|
||||
assert!(!visible(&s, "UIParent"), "SetFullScreenFrame hid UIParent");
|
||||
assert!(
|
||||
!visible(&s, "StaticPopup1"),
|
||||
"and the hide cascaded to its children — this is the whole point of the 72 restored \
|
||||
parent= declarations, and the assertion that fails if one is dropped again"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return StaticPopup1:IsShown() and 1 or 0")
|
||||
.unwrap(),
|
||||
1,
|
||||
"cascaded, not shown=false: the frame keeps its own state and gets it back untouched"
|
||||
);
|
||||
assert!(
|
||||
visible(&s, "CinematicFrame"),
|
||||
"**and the fly-by itself survives** — the reference declares CinematicFrame with no \
|
||||
parent precisely so the frame being shown escapes the hide that showing it performs"
|
||||
);
|
||||
|
||||
s.set_in_cinematic(false);
|
||||
s.fire_event("CINEMATIC_STOP", vec![]);
|
||||
s.resolve();
|
||||
assert!(visible(&s, "UIParent"), "and the world's UI comes back");
|
||||
assert!(visible(&s, "StaticPopup1"), "with the child that was up");
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ fn harness_with(extra: &[&str]) -> UiScript {
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
let files: Vec<&str> = [
|
||||
"Fonts.xml",
|
||||
// `GameMenuFrame` and the panels it opens declare `parent="UIParent"`, resolved at LOAD
|
||||
// (decision 1734) — UIParent must already be there, as it is in the manifest.
|
||||
"UIParent.xml",
|
||||
"MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"UIPanelTemplates.xml",
|
||||
|
||||
@@ -172,4 +172,34 @@ mod tests {
|
||||
shipped.sort();
|
||||
assert_eq!(listed, shipped);
|
||||
}
|
||||
|
||||
/// **Nothing may declare `parent="UIParent"` before `UIParent.xml` has loaded.**
|
||||
///
|
||||
/// A parent name is resolved at LOAD, and a name that does not exist yet is not an error: the
|
||||
/// loader warns and silently falls back to the enclosing frame. So this ordering mistake does
|
||||
/// not fail, it *half-works* — the frame keeps drawing, keeps answering `IsShown`, and simply
|
||||
/// never joins the cascade `UIParent:Hide()` walks. That is precisely how it would be missed.
|
||||
///
|
||||
/// It nearly was: `UIParent.xml` sat below `UiPanels.xml` until decision 1734, so restoring
|
||||
/// `StaticPopup1`/`StaticPopup2`'s parents there would have written two declarations that did
|
||||
/// nothing at all. The reference's own order is the fix (FrameXML.toc: BasicControls.xml l.6,
|
||||
/// UIParent.xml l.8), and this keeps it.
|
||||
#[test]
|
||||
fn nothing_declares_a_uiparent_child_before_uiparent_itself_loads() {
|
||||
let files = manifest_files();
|
||||
let at = files
|
||||
.iter()
|
||||
.position(|f| f == "UIParent.xml")
|
||||
.expect("the manifest lists UIParent.xml");
|
||||
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui");
|
||||
for early in &files[..at] {
|
||||
let text = std::fs::read_to_string(dir.join(early)).unwrap();
|
||||
assert!(
|
||||
!text.contains(r#"parent="UIParent""#),
|
||||
"{early} loads before UIParent.xml (position {at}) but declares a \
|
||||
UIParent child — the loader would warn and silently drop it. Move \
|
||||
UIParent.xml up, or the declaration down."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,26 +450,14 @@ fn capture_ui_active(capture: Option<Res<crate::run_mode::CaptureMode>>) -> bool
|
||||
fn arbitrate_pointer_over_ui(
|
||||
egui: Option<Res<EguiPointerOver>>,
|
||||
hover: Res<PlayerUiHover>,
|
||||
cinematic: Option<Res<crate::cinematic::Cinematic>>,
|
||||
mut over: ResMut<PointerOverUi>,
|
||||
) {
|
||||
// **A cinematic covers the screen with a frame, so the pointer is over the UI wherever it
|
||||
// is.** That is a fact about the reference's own layout, not a special case: `CinematicFrame`
|
||||
// is `setAllPoints` + `enableMouse="true"`, and while it is up it is the only mouse target the
|
||||
// hit test can reach — because `UIParent:Hide()` has taken every other frame out of it.
|
||||
//
|
||||
// benilla cannot yet reach that answer through the hit test. `crate::cinematic` substitutes
|
||||
// `UiHidden` for the reference's `UIParent:Hide()` (decision 1699, for want of a HUD parented
|
||||
// to `UIParent`), and `UiHidden` also switches the whole mouse feed off — ALT-Z's meaning,
|
||||
// where the world *should* take the mouse. So `feed_ui_input` never hit-tested
|
||||
// `CinematicFrame` at all and the world took every click of a fly-by: right-drag turned the
|
||||
// character, left-click picked targets through the flying camera, the wheel zoomed a rig
|
||||
// nobody could see. **This line states the layout fact directly, and it is the interim, not
|
||||
// the fix** — restoring the 32 dropped `parent="UIParent"` declarations lets the hit test
|
||||
// reach the same answer on its own, and this argument (and this parameter) is then dead code
|
||||
// to delete.
|
||||
let in_cinematic = cinematic.is_some_and(|c| c.is_playing());
|
||||
over.0 = in_cinematic || egui.is_some_and(|e| e.0) || hover.0.is_some();
|
||||
// A cinematic used to need a third term here, and no longer does — the deletion is the point
|
||||
// of decision 1734. `CinematicFrame` is `setAllPoints` + `enableMouse="true"`, so while it is
|
||||
// up it is the only mouse target the hit test can reach, because `UIParent:Hide()` has taken
|
||||
// every other frame out of it. The hit test now arrives at that on its own, so
|
||||
// [`PlayerUiHover`] carries it and the special case is gone.
|
||||
over.0 = egui.is_some_and(|e| e.0) || hover.0.is_some();
|
||||
}
|
||||
|
||||
/// The session lifecycle — the VM's birth, identity, death, and the reload. Every edge function
|
||||
|
||||
@@ -38,6 +38,9 @@ fn harness_on(mut s: UiScript) -> UiScript {
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
for file in [
|
||||
"Fonts.xml",
|
||||
// Every panel window declares `parent="UIParent"`, resolved at LOAD — so UIParent has to
|
||||
// exist by the time they are read, exactly as it does in the manifest (decision 1734).
|
||||
"UIParent.xml",
|
||||
"MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"GameTooltip.xml",
|
||||
|
||||
@@ -1089,3 +1089,111 @@ fn the_inspect_cursor_pair_takes_both_arms() {
|
||||
s.errors()
|
||||
);
|
||||
}
|
||||
|
||||
/// **Nothing of the interface is left on screen during a cinematic** — swept over the whole
|
||||
/// shipped manifest, not one file at a time.
|
||||
///
|
||||
/// This is the test the director's eye had to stand in for. Decision 1734 restored 72 dropped
|
||||
/// `parent=` declarations and a per-window test proved the cascade worked; the chat still drew
|
||||
/// over the fly-by, because benilla carries the reference's `FloatingChatFrameTemplate` and
|
||||
/// `ChatTabTemplate` under its own names (`BenillaChatFrameTemplate`, `BenillaChatTabTemplate`)
|
||||
/// and the gap analysis that found the 72 skipped `virtual="true"` templates entirely. A
|
||||
/// name-for-name comparison against the reference could not see it. **Sweeping what is actually
|
||||
/// visible can**, which is why this is written against the observable and not against a list.
|
||||
///
|
||||
/// The three survivors are each required to survive:
|
||||
///
|
||||
/// - `CinematicFrame` — the frame being *shown*. The reference declares it with no parent for
|
||||
/// exactly this reason, and `SetFullScreenFrame` shows it in the same breath as hiding UIParent.
|
||||
/// - `WorldFrame` — the 3D scene's frame. Ours renders nothing (Bevy draws the world), but it is
|
||||
/// the reference's own bottom-of-strata frame and a cinematic is a thing you watch *in* it.
|
||||
/// - `BenillaFadeDriver` — a 1x1 frame with no textures and no layers, whose only content is an
|
||||
/// `OnUpdate` running `UIFrameFadeUpdate`. It draws nothing, and it must not be hidden: a hidden
|
||||
/// frame's `OnUpdate` does not run, so parenting it would freeze every in-flight `UIFrameFade`
|
||||
/// the instant a cinematic started and leave frames stranded mid-fade.
|
||||
#[test]
|
||||
fn a_cinematic_leaves_nothing_of_the_interface_on_screen() {
|
||||
let mut s = benilla_ui::script::UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
let failures = super::load_default_ui(&s);
|
||||
assert!(failures.is_empty(), "manifest load errors: {failures:#?}");
|
||||
s.resolve();
|
||||
|
||||
let names = shipped_frame_names();
|
||||
let visible = |s: &benilla_ui::script::UiScript, n: &str| -> bool {
|
||||
s.eval::<i64>(&format!(
|
||||
"local f = getglobal(\"{n}\") \
|
||||
if not f or not f.IsVisible then return 0 end \
|
||||
return f:IsVisible() and 1 or 0"
|
||||
))
|
||||
.unwrap_or(0)
|
||||
== 1
|
||||
};
|
||||
|
||||
// The sweep is only worth anything if there was something to hide in the first place.
|
||||
let before = names.iter().filter(|n| visible(&s, n)).count();
|
||||
assert!(
|
||||
before > 50,
|
||||
"only {before} frames visible before the cinematic — the sweep found no interface to \
|
||||
hide, so it would pass no matter what the cascade did"
|
||||
);
|
||||
|
||||
s.set_in_cinematic(true);
|
||||
s.fire_event("CINEMATIC_START", vec![]);
|
||||
s.resolve();
|
||||
|
||||
let mut after: Vec<&str> = names
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.filter(|n| visible(&s, n))
|
||||
.collect();
|
||||
after.sort_unstable();
|
||||
assert_eq!(
|
||||
after,
|
||||
["BenillaFadeDriver", "CinematicFrame", "WorldFrame"],
|
||||
"something is drawing over the fly-by (see this test's header for why exactly these \
|
||||
three are allowed to survive)"
|
||||
);
|
||||
|
||||
// …and the player gets it all back.
|
||||
s.set_in_cinematic(false);
|
||||
s.fire_event("CINEMATIC_STOP", vec![]);
|
||||
s.resolve();
|
||||
let restored = names.iter().filter(|n| visible(&s, n)).count();
|
||||
assert_eq!(
|
||||
restored, before,
|
||||
"the interface comes back exactly as it was"
|
||||
);
|
||||
}
|
||||
|
||||
/// Every named, non-virtual frame the shipped tree declares — the sweep's population.
|
||||
fn shipped_frame_names() -> Vec<String> {
|
||||
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui");
|
||||
let mut names: Vec<String> = Vec::new();
|
||||
for entry in std::fs::read_dir(&dir).expect("assets/ui").flatten() {
|
||||
let path = entry.path();
|
||||
if !path.extension().is_some_and(|e| e == "xml") {
|
||||
continue;
|
||||
}
|
||||
let text = std::fs::read_to_string(&path).expect("read");
|
||||
for chunk in text.split('<').skip(1) {
|
||||
let head = &chunk[..chunk.find('>').unwrap_or(chunk.len())];
|
||||
if head.contains("virtual=\"true\"") {
|
||||
continue;
|
||||
}
|
||||
let Some(i) = head.find("name=\"") else {
|
||||
continue;
|
||||
};
|
||||
let rest = &head[i + 6..];
|
||||
let Some(j) = rest.find('"') else { continue };
|
||||
let n = &rest[..j];
|
||||
// `$parent`-templated names are not globals; nothing can look them up by name.
|
||||
if !n.is_empty() && !n.contains('$') {
|
||||
names.push(n.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
names.sort();
|
||||
names.dedup();
|
||||
names
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user