Files
benilla-octo/crates/benilla-protocol/tests/instance.rs
T
SamandClaude Fable 5 03c70a7035 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>
2026-08-31 20:17:25 +07:00

173 lines
7.1 KiB
Rust

//! Oracle-free golden tests for the instance/raid **lockout message** family (decision 1748): the
//! six inbound bodies, the one outbound body, and the two enums the client's own dispatch tables
//! define.
//!
//! Every layout here is the *client's* read order, taken from the five handlers registered at
//! `0x498680`-`0x4986cf` plus `0x4e7e60`, and cross-checked against vmangos
//! `Server/Packets/Misc.{h,cpp}`. `hx(...)` bodies round-tripped through `parse_server`, the
//! idiom of `tests/bank.rs`.
use benilla_protocol::events::{decode, SessionEvent};
use benilla_protocol::messages::{
self, opcode, InstanceResetFailure, RaidInstanceMessage, RaidInstanceWarning,
};
use benilla_protocol::ServerPacket;
fn hx(s: &str) -> Vec<u8> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
.collect()
}
/// `CMSG_RESET_INSTANCES` — empty, both in the binding (`0x48a6b0` writes no field before its
/// send) and in vmangos's handler (which reads none).
#[test]
fn reset_instances_body_is_empty() {
assert_eq!(messages::reset_instances(), Vec::<u8>::new());
}
/// `SMSG_RAID_INSTANCE_MESSAGE`: `u32 type`, `u32 mapId`, `u32 secondsUntilReset`, in that order
/// — the client reads the three at `0x49e1cd`/`0x49e1d8`/`0x49e1e3` and uses the SECOND for the
/// `Map.dbc` lookup, which is what fixes the order (a type/map swap would name the wrong instance
/// and pick the wrong template, silently).
#[test]
fn raid_instance_message_wire() {
// type 4 (WELCOME), map 409 (Molten Core), 3 d 2 h 0 m = 266400 s.
let body = hx(concat!("04000000", "99010000", "a0100400"));
let p = messages::parse_server(opcode::SMSG_RAID_INSTANCE_MESSAGE, &body).unwrap();
match &p {
ServerPacket::RaidInstanceMessage { message } => {
assert_eq!(
*message,
RaidInstanceMessage {
message_type: 4,
map: 409,
reset: 266_400,
}
);
}
other => panic!("expected RaidInstanceMessage, got {}", other.name()),
}
match decode(p).as_slice() {
[SessionEvent::RaidInstanceMessage { message }] => {
assert_eq!(message.map, 409);
assert_eq!(message.reset, 266_400);
}
other => panic!("raid instance message decode: {other:?}"),
}
// A body one field short is a parse failure, not a zero-filled message.
assert!(
messages::parse_server(opcode::SMSG_RAID_INSTANCE_MESSAGE, &hx("0400000099010000"))
.is_err()
);
}
/// The four warning types and the two the reference's jump table drops. `dec eax; cmp eax,3; ja`
/// at `0x49e246` means 0 and ≥ 5 leave the handler in silence — including the value later clients
/// call `RAID_INSTANCE_EXPIRED` (5).
#[test]
fn raid_instance_warning_types() {
use RaidInstanceWarning as W;
assert_eq!(W::from_wire(1), Some(W::Hours));
assert_eq!(W::from_wire(2), Some(W::Minutes));
assert_eq!(W::from_wire(3), Some(W::MinutesSoon));
assert_eq!(W::from_wire(4), Some(W::Welcome));
assert_eq!(W::from_wire(0), None);
assert_eq!(
W::from_wire(5),
None,
"RAID_INSTANCE_EXPIRED prints nothing"
);
assert_eq!(W::from_wire(u32::MAX), None);
assert_eq!(W::Hours.token(), "RAID_INSTANCE_WARNING_HOURS");
assert_eq!(W::Minutes.token(), "RAID_INSTANCE_WARNING_MIN");
assert_eq!(W::MinutesSoon.token(), "RAID_INSTANCE_WARNING_MIN_SOON");
assert_eq!(W::Welcome.token(), "RAID_INSTANCE_WELCOME");
}
/// `SMSG_INSTANCE_RESET`: one `u32` map id (`0x49e481`).
#[test]
fn instance_reset_wire() {
let p = messages::parse_server(opcode::SMSG_INSTANCE_RESET, &hx("24000000")).unwrap();
match &p {
ServerPacket::InstanceReset { map } => assert_eq!(*map, 36), // Deadmines
other => panic!("expected InstanceReset, got {}", other.name()),
}
match decode(p).as_slice() {
[SessionEvent::InstanceReset { map: 36 }] => {}
other => panic!("instance reset decode: {other:?}"),
}
assert!(messages::parse_server(opcode::SMSG_INSTANCE_RESET, &hx("2400")).is_err());
}
/// `SMSG_INSTANCE_RESET_FAILED`: `u32 reason` FIRST, then `u32 mapId` (`0x49e54d`/`0x49e558`) —
/// the opposite order to the reset success packet's single field, and the pair vmangos writes in
/// `InstanceResetFailed::AppendBodyTo`.
#[test]
fn instance_reset_failed_wire() {
let body = hx(concat!("01000000", "24000000")); // OFFLINE, Deadmines
let p = messages::parse_server(opcode::SMSG_INSTANCE_RESET_FAILED, &body).unwrap();
match &p {
ServerPacket::InstanceResetFailed { failure } => {
assert_eq!(failure.reason, 1);
assert_eq!(failure.map, 36);
}
other => panic!("expected InstanceResetFailed, got {}", other.name()),
}
match decode(p).as_slice() {
[SessionEvent::InstanceResetFailed { failure }] => {
assert_eq!(
InstanceResetFailure::from_wire(failure.reason),
Some(InstanceResetFailure::Offline)
);
}
other => panic!("instance reset failed decode: {other:?}"),
}
}
/// The three refusal reasons, and the silent tail. vmangos's own enum names 3
/// `INSTANCERESET_FAIL_SILENTLY` "as well as any above this"; the reference falls through those
/// to its chat call with an uninitialized buffer, which we deliberately do not reproduce.
#[test]
fn instance_reset_failure_reasons() {
use InstanceResetFailure as F;
assert_eq!(F::from_wire(0), Some(F::General));
assert_eq!(F::from_wire(1), Some(F::Offline));
assert_eq!(F::from_wire(2), Some(F::Zoning));
assert_eq!(F::from_wire(3), None, "INSTANCERESET_FAIL_SILENTLY");
assert_eq!(F::from_wire(99), None);
assert_eq!(F::General.token(), "INSTANCE_RESET_FAILED");
assert_eq!(F::Offline.token(), "INSTANCE_RESET_FAILED_OFFLINE");
assert_eq!(F::Zoning.token(), "INSTANCE_RESET_FAILED_ZONING");
}
/// The three bare-`u32` bodies that are not a map name: the save-created flag (`0x4e7e6c`), the
/// last-instance map (`0x49e676`) and the ownership flag (`0x49e6c6`). The ownership flag is
/// narrowed to a bool at the event layer because the reference's reader is a `test eax,eax`.
#[test]
fn save_created_last_instance_and_ownership_wire() {
let p = messages::parse_server(opcode::SMSG_INSTANCE_SAVE_CREATED, &hx("00000000")).unwrap();
match decode(p).as_slice() {
[SessionEvent::InstanceSaveCreated { flag: 0 }] => {}
other => panic!("save created decode: {other:?}"),
}
let p = messages::parse_server(opcode::SMSG_UPDATE_LAST_INSTANCE, &hx("99010000")).unwrap();
match decode(p).as_slice() {
[SessionEvent::UpdateLastInstance { map: 409 }] => {}
other => panic!("last instance decode: {other:?}"),
}
for (body, owns) in [("00000000", false), ("01000000", true), ("07000000", true)] {
let p = messages::parse_server(opcode::SMSG_UPDATE_INSTANCE_OWNERSHIP, &hx(body)).unwrap();
match decode(p).as_slice() {
[SessionEvent::UpdateInstanceOwnership { owns: got }] => assert_eq!(*got, owns),
other => panic!("ownership decode: {other:?}"),
}
}
}