the bag windows are the real 1.12 files, and /walk actually walks
- the bag windows are the reference's own `ContainerFrame.xml` running off the player's patch chain — decision 1751 makes the stock 1.12 UI off the install the end state and `assets/ui` scaffolding that retires file-by-file (86 files against the 99-file stock corpus, 72% of the 1128-function global surface already built) — via one ordered manifest, `benilla.toc`, where a bare name is a file we ship and an `Interface\FrameXML\` path is the player's own file; the windows on screen are `ContainerFrame1..12`, the bank's six copy-pasted popouts fold into them at ids 5..10, net −750 lines, and the swap forced real engine verbs into being rather than stubs (`SetBagPortaitTexture` with its id-0 no-texture arm, `GameTooltip:AppendText`, `SetButtonPulse`, and a `<Model>` whose file is the cooldown indicator materializing as the native sweep, which three corpus addons inherit) - the 1.12 walk toggle lands: `TOGGLERUN` latches one bit with `ToggleRun`'s own silent refusal chain (dead, rooted, on a live server spline), and the walk animation was never missing at all — the flag reaches it through exactly one channel, the speed (`0x5fd100` picks Walk over Run on `speed > 2 × walkSpeed`); pinning it caught our remote extrapolator with the two min rungs backwards — `min(walk, run)` runs BEFORE the backward min, so a walking backpedaller moves at 2.5 yd/s where we would have played them the Run clip at 4.5 — and one shared `current_speed` now keeps the local controller and the extrapolator from ever disagreeing about it - the instance and raid lockout messages exist — six packets that were dropped as unknown opcodes now land, four of them as the CHAT_MSG_SYSTEM lines the client composes itself, which is why no Lua handler ever surfaced them — plus `IsInInstance`, `ResetInstances`, and the unit popup's "Reset all instances" row behind the reference's own predicate; where the binary contradicts vmangos: the ownership flag means "any instance bind" but vmangos only ever names raid binds, so the row was dead for every character without a raid lockout — term 1 is now also satisfied by our own eyes, walking out of a party dungeon the server itself bound us to - opening the world map showed 3D world at the sheet's margins: `BlackoutWorld` was parented to the very `UIParent` a fullscreen panel hides, and three more frames had crossed the same boundary — the shared dropdown lists (so the map's continent and zone pickers answered a click with nothing at all), the master-looter menu, and the screenshot confirmation, which could never appear during a cinematic; `frame_flag_gate` now checks the parent seat against the reference both ways a frame acquires one (the attribute, template-inherited included, and `<Frames>` nesting), and its first harvest took 30 divergences down to three accepted ones Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
//! The instance-lockout **Era surface** (decision 1748): the three engine bindings, and the one
|
||||
//! place in the shipped UI that reads them — the SELF menu's "Reset all instances" row and the
|
||||
//! `CONFIRM_RESET_INSTANCES` dialog behind it.
|
||||
//!
|
||||
//! The four chat lines the family's other packets raise are engine-composed and have no Lua at
|
||||
//! all (no FrameXML file mentions `RAID_INSTANCE_WELCOME` or its siblings); they are tested at the
|
||||
//! composer, in `crate::ui_instance`.
|
||||
|
||||
use benilla_ui::script::UiScript;
|
||||
|
||||
/// Load one shipped `assets/ui/<file>` into `s`, panicking on any loader error.
|
||||
fn load_xml(s: &UiScript, file: &str) {
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(file),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(s, &doc, &|_| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"{file}: loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
}
|
||||
|
||||
/// The three bindings answer with the reference's own shapes — `IsInInstance` a PAIR, both
|
||||
/// readers `1`/`nil` rather than `true`/`false` (the reference pushes the double 1.0), and
|
||||
/// `ResetInstances` a queue.
|
||||
#[test]
|
||||
fn the_three_bindings_have_the_reference_shapes() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
|
||||
// Nothing pushed yet: the app has not said where we are.
|
||||
assert_eq!(
|
||||
s.eval::<(Option<f64>, String)>("return IsInInstance()")
|
||||
.unwrap(),
|
||||
(None, "none".into()),
|
||||
"no map pushed reads as not-an-instance"
|
||||
);
|
||||
|
||||
for (ty, inside, name) in [
|
||||
(0u32, None, "none"),
|
||||
(1, Some(1.0), "party"),
|
||||
(2, Some(1.0), "raid"),
|
||||
(3, Some(1.0), "pvp"),
|
||||
// Past the reference's own `cmp esi,4; jae` guard.
|
||||
(4, Some(1.0), "none"),
|
||||
] {
|
||||
s.set_instance_type(Some(ty));
|
||||
assert_eq!(
|
||||
s.eval::<(Option<f64>, String)>("return IsInInstance()")
|
||||
.unwrap(),
|
||||
(inside, name.into()),
|
||||
"InstanceType {ty}"
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
s.eval::<Option<f64>>("return CanShowResetInstances()")
|
||||
.unwrap(),
|
||||
None,
|
||||
"false by default — no bind, no dungeon behind us"
|
||||
);
|
||||
s.set_can_reset_instances(true);
|
||||
assert_eq!(
|
||||
s.eval::<Option<f64>>("return CanShowResetInstances()")
|
||||
.unwrap(),
|
||||
Some(1.0)
|
||||
);
|
||||
|
||||
assert_eq!(s.take_reset_instance_asks(), 0);
|
||||
s.run("ResetInstances(); ResetInstances()").unwrap();
|
||||
assert_eq!(s.take_reset_instance_asks(), 2, "each call is one send");
|
||||
assert_eq!(s.take_reset_instance_asks(), 0, "the drain is a take");
|
||||
}
|
||||
|
||||
/// The whole shipped path, through the real hit test: right-click your own portrait, the row is
|
||||
/// there only while `CanShowResetInstances()` is true, clicking it raises the reference's confirm,
|
||||
/// and Yes is the only thing that sends.
|
||||
#[test]
|
||||
fn the_self_menu_row_gates_on_the_binding_and_confirms_before_sending() {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
// The row labels a bare harness has no GlobalStrings.lua for. Verbatim 1.12 values
|
||||
// (`RESET_INSTANCES` l.3342, `CONFIRM_RESET_INSTANCES` l.851, `YES` l.5463, `NO` l.2794) —
|
||||
// production runs the player's own string table at boot.
|
||||
s.run(
|
||||
r#"
|
||||
RESET_INSTANCES = "Reset all instances"
|
||||
CONFIRM_RESET_INSTANCES = "Do you really want to reset all of your instances?"
|
||||
YES = "Yes"
|
||||
NO = "No"
|
||||
CANCEL = "Cancel"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
for file in [
|
||||
"Fonts.xml",
|
||||
"UIParent.xml",
|
||||
"MoneyFrame.xml",
|
||||
"UiPanels.xml",
|
||||
"GameTooltip.xml",
|
||||
"UIDropDownMenu.xml",
|
||||
"UnitPopup.xml",
|
||||
"UnitFrames.xml",
|
||||
"RaidFrame.xml",
|
||||
] {
|
||||
load_xml(&s, file);
|
||||
}
|
||||
s.resolve();
|
||||
assert!(s.errors().is_empty(), "load errors: {:?}", s.errors());
|
||||
|
||||
let open_self_menu = |s: &mut UiScript| {
|
||||
s.run("CloseDropDownMenus()").unwrap();
|
||||
s.run(r#"PlayerFrame_OnClick("RightButton")"#).unwrap();
|
||||
s.resolve();
|
||||
};
|
||||
let row_labels = |s: &mut UiScript| -> Vec<String> {
|
||||
let n = s.eval::<i64>("return DropDownList1.numButtons").unwrap();
|
||||
(1..=n)
|
||||
.map(|i| {
|
||||
s.eval::<String>(&format!("return DropDownList1Button{i}:GetText() or \"\""))
|
||||
.unwrap()
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
// Off: the row is not in the menu at all — hidden, not greyed (the reference's
|
||||
// `UnitPopupShown[index] = 0`). Solo, it is the ONLY row of the SELF menu that could show
|
||||
// (the loot trio, Leave and the raid marks all need a party), so the menu's "nothing but
|
||||
// CANCEL" early-out fires and no menu opens at all. That is the reference's behaviour too,
|
||||
// and it is the sharpest possible assertion that the row is really gone.
|
||||
open_self_menu(&mut s);
|
||||
assert!(
|
||||
s.eval::<bool>("return not DropDownList1:IsVisible()")
|
||||
.unwrap(),
|
||||
"solo with no lockout to reset, the SELF menu has nothing to show"
|
||||
);
|
||||
|
||||
// On: the row appears, and with it the menu.
|
||||
s.set_can_reset_instances(true);
|
||||
open_self_menu(&mut s);
|
||||
assert!(
|
||||
s.eval::<bool>("return DropDownList1:IsVisible()").unwrap(),
|
||||
"the one showable row is enough to open the SELF menu"
|
||||
);
|
||||
let labels = row_labels(&mut s);
|
||||
let row = labels
|
||||
.iter()
|
||||
.position(|l| l == "Reset all instances")
|
||||
.unwrap_or_else(|| panic!("the row shows once the binding says so: {labels:?}"));
|
||||
|
||||
// Clicking it sends NOTHING — it raises the confirm.
|
||||
let button = format!("DropDownList1Button{}", row + 1);
|
||||
let (cx, cy) = s
|
||||
.eval::<(f64, f64)>(&format!("return {button}:GetCenter()"))
|
||||
.unwrap();
|
||||
s.mouse_button(cx as f32, cy as f32, "LeftButton", true);
|
||||
s.mouse_button(cx as f32, cy as f32, "LeftButton", false);
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
s.take_reset_instance_asks(),
|
||||
0,
|
||||
"the row itself never sends — it asks"
|
||||
);
|
||||
assert!(
|
||||
s.eval::<bool>("return StaticPopup1:IsVisible()").unwrap(),
|
||||
"the row raises CONFIRM_RESET_INSTANCES"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return StaticPopup1Text:GetText()")
|
||||
.unwrap(),
|
||||
"Do you really want to reset all of your instances?"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return StaticPopup1Button1:GetText()")
|
||||
.unwrap(),
|
||||
"Yes"
|
||||
);
|
||||
assert_eq!(
|
||||
s.eval::<String>("return StaticPopup1Button2:GetText()")
|
||||
.unwrap(),
|
||||
"No"
|
||||
);
|
||||
|
||||
// No sends nothing; Yes sends exactly one.
|
||||
let (nx, ny) = s
|
||||
.eval::<(f64, f64)>("return StaticPopup1Button2:GetCenter()")
|
||||
.unwrap();
|
||||
s.mouse_button(nx as f32, ny as f32, "LeftButton", true);
|
||||
s.mouse_button(nx as f32, ny as f32, "LeftButton", false);
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
s.take_reset_instance_asks(),
|
||||
0,
|
||||
"No is not an answer that sends"
|
||||
);
|
||||
|
||||
open_self_menu(&mut s);
|
||||
let labels = row_labels(&mut s);
|
||||
let row = labels
|
||||
.iter()
|
||||
.position(|l| l == "Reset all instances")
|
||||
.expect("still offered");
|
||||
let button = format!("DropDownList1Button{}", row + 1);
|
||||
let (cx, cy) = s
|
||||
.eval::<(f64, f64)>(&format!("return {button}:GetCenter()"))
|
||||
.unwrap();
|
||||
s.mouse_button(cx as f32, cy as f32, "LeftButton", true);
|
||||
s.mouse_button(cx as f32, cy as f32, "LeftButton", false);
|
||||
s.resolve();
|
||||
let (yx, yy) = s
|
||||
.eval::<(f64, f64)>("return StaticPopup1Button1:GetCenter()")
|
||||
.unwrap();
|
||||
s.mouse_button(yx as f32, yy as f32, "LeftButton", true);
|
||||
s.mouse_button(yx as f32, yy as f32, "LeftButton", false);
|
||||
s.resolve();
|
||||
assert_eq!(
|
||||
s.take_reset_instance_asks(),
|
||||
1,
|
||||
"Yes is the one call that sends CMSG_RESET_INSTANCES"
|
||||
);
|
||||
assert!(s.errors().is_empty(), "script errors: {:?}", s.errors());
|
||||
}
|
||||
Reference in New Issue
Block a user