Files
benilla-octo/crates/benilla-protocol/src/messages/instance.rs
T
SamandClaude Fable 5 67315143f9 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

169 lines
7.9 KiB
Rust

//! The instance/raid **lockout message** family — the six server packets the 1.12 client turns
//! into a `CHAT_MSG_SYSTEM` line or into its own lockout bookkeeping, plus the one thing the
//! player can ask for (`CMSG_RESET_INSTANCES`). Decision 1748.
//!
//! | opcode | direction | body | what the client does with it |
//! |---|---|---|---|
//! | `SMSG_INSTANCE_SAVE_CREATED` 0x2cb | in | `u32` flag | chat line `INSTANCE_SAVED` |
//! | `SMSG_RAID_INSTANCE_MESSAGE` 0x2fa | in | `u32` type, `u32` mapId, `u32` secsToReset | chat line, one of four |
//! | `CMSG_RESET_INSTANCES` 0x31d | out | empty | — |
//! | `SMSG_INSTANCE_RESET` 0x31e | in | `u32` mapId | chat line `INSTANCE_RESET_SUCCESS` + clears the last-instance latch |
//! | `SMSG_INSTANCE_RESET_FAILED` 0x31f | in | `u32` reason, `u32` mapId | chat line, one of three |
//! | `SMSG_UPDATE_LAST_INSTANCE` 0x320 | in | `u32` mapId | records the last dungeon we were in |
//! | `SMSG_UPDATE_INSTANCE_OWNERSHIP` 0x32b | in | `u32` flag | the "do I hold any permanent bind" latch |
//!
//! **The client side is VERIFIED at the bytes** (WoW.exe build 5875): the six handlers are
//! registered together at `0x498680`-`0x4986cf` (`0x49e1c0` 0x2fa, `0x49e470` 0x31e, `0x49e540`
//! 0x31f, `0x49e670` 0x320, `0x49e6c0` 0x32b) with the save-created one registered apart, in the
//! chat-spam-filter TU, at `0x4e7e48` (`0x4e7e60`). Every one reads its fields with
//! `CDataStore::GetInt32 0x418e30` in the order below, so the layouts here are the client's own
//! read order, not an inference from the server. The server side is VERIFIED vmangos
//! `Server/Packets/Misc.{h,cpp}` (`RaidInstanceMessage`, `InstanceReset`, `InstanceResetFailed`,
//! `UpdateLastInstance`, `UpdateInstanceOwnership`) + `Maps/Map.cpp:2288,2388` for the
//! save-created flag, and the two agree field for field.
//!
//! **No map NAME is ever on the wire** — every one of these carries a `Map.dbc` id and the client
//! looks the display name up itself (`[0xc0daa8][id]`, field `+0x10` = `MapName_Lang[locale]`),
//! falling back to the id printed through `"%d"` when the row is missing. Ours resolves through
//! `MapCatalog::name` with the same fallback.
use std::io;
use crate::wire::read_u32_le;
/// `SMSG_RAID_INSTANCE_MESSAGE`'s `type` — which of the four warning templates the line uses
/// (VERIFIED: the client's `switch (type - 1)` jump table at `0x49e45c`, four entries; the same
/// numbering vmangos writes in `Objects/Player.h:568-573`).
///
/// **Type 5 (`RAID_INSTANCE_EXPIRED` in later clients) and type 0 print nothing**: the dispatch is
/// `dec eax; cmp eax,3; ja <return>`, so anything outside 1..=4 leaves the handler silently.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RaidInstanceWarning {
/// 1 — `RAID_INSTANCE_WARNING_HOURS`, filled with `resetTime / 3600`.
Hours,
/// 2 — `RAID_INSTANCE_WARNING_MIN`, filled with `resetTime / 60`.
Minutes,
/// 3 — `RAID_INSTANCE_WARNING_MIN_SOON`, filled with `resetTime / 60`.
MinutesSoon,
/// 4 — `RAID_INSTANCE_WELCOME`, filled with the `d`/`h`/`m` breakdown of `resetTime`.
Welcome,
}
impl RaidInstanceWarning {
/// The wire `type` → the template, or `None` for the values the reference's jump table drops
/// (0 and ≥ 5). A dropped type is not an error: the packet is well-formed, the client just
/// has no line for it.
pub fn from_wire(ty: u32) -> Option<Self> {
match ty {
1 => Some(Self::Hours),
2 => Some(Self::Minutes),
3 => Some(Self::MinutesSoon),
4 => Some(Self::Welcome),
_ => None,
}
}
/// The GlobalStrings token the reference passes to `GetText` for this template (the four
/// `mov ecx, <string>` at `0x49e276`/`0x49e2e5`/`0x49e354`/`0x49e40b`).
pub fn token(self) -> &'static str {
match self {
Self::Hours => "RAID_INSTANCE_WARNING_HOURS",
Self::Minutes => "RAID_INSTANCE_WARNING_MIN",
Self::MinutesSoon => "RAID_INSTANCE_WARNING_MIN_SOON",
Self::Welcome => "RAID_INSTANCE_WELCOME",
}
}
}
/// `SMSG_RAID_INSTANCE_MESSAGE` — a raid lockout's welcome/countdown line.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RaidInstanceMessage {
/// The raw wire type. Kept raw rather than pre-resolved so a type the reference drops still
/// round-trips through the event layer (and so a golden can assert the drop).
pub message_type: u32,
/// `Map.dbc` id of the instance the warning is about.
pub map: u32,
/// **Seconds remaining** until the reset, not a timestamp (vmangos passes
/// `resetTime` = a remaining duration into `SendInstanceResetWarning`).
pub reset: u32,
}
/// `SMSG_INSTANCE_RESET_FAILED`'s `reason` — which of the three refusal templates the line uses
/// (VERIFIED: the client's `sub eax,0; je / dec eax; je / dec eax; jne` ladder at `0x49e5b7`;
/// vmangos `Maps/MapPersistentStateMgr.h:270-276`, whose fourth value `_SILENTLY` = 3 is
/// deliberately outside the ladder).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InstanceResetFailure {
/// 0 — `INSTANCE_RESET_FAILED`: players still inside.
General,
/// 1 — `INSTANCE_RESET_FAILED_OFFLINE`: someone in the party is offline.
Offline,
/// 2 — `INSTANCE_RESET_FAILED_ZONING`: someone in the party is zoning in.
Zoning,
}
impl InstanceResetFailure {
/// The wire `reason` → the template, or `None` for ≥ 3 (`INSTANCERESET_FAIL_SILENTLY` and
/// above). The reference *falls through* those to its chat call with an **uninitialized**
/// stack buffer — a real 1.12 bug we do not reproduce: silence is what the server means by
/// them, and printing a garbage line is not fidelity to an intent, only to an accident.
pub fn from_wire(reason: u32) -> Option<Self> {
match reason {
0 => Some(Self::General),
1 => Some(Self::Offline),
2 => Some(Self::Zoning),
_ => None,
}
}
/// The GlobalStrings token (the three `mov ecx, <string>` at `0x49e61b`/`0x49e5f6`/`0x49e5d1`).
pub fn token(self) -> &'static str {
match self {
Self::General => "INSTANCE_RESET_FAILED",
Self::Offline => "INSTANCE_RESET_FAILED_OFFLINE",
Self::Zoning => "INSTANCE_RESET_FAILED_ZONING",
}
}
}
/// `SMSG_INSTANCE_RESET_FAILED` — the group leader's reset was refused.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InstanceResetFailed {
/// The raw wire reason (see [`InstanceResetFailure::from_wire`]).
pub reason: u32,
/// `Map.dbc` id of the instance that would not reset.
pub map: u32,
}
/// Read `SMSG_RAID_INSTANCE_MESSAGE`: `u32 type`, `u32 mapId`, `u32 secondsUntilReset` — the
/// client's own read order at `0x49e1cd`/`0x49e1d8`/`0x49e1e3`.
pub(super) fn read_raid_instance_message(r: &mut &[u8]) -> io::Result<RaidInstanceMessage> {
Ok(RaidInstanceMessage {
message_type: read_u32_le(r)?,
map: read_u32_le(r)?,
reset: read_u32_le(r)?,
})
}
/// Read `SMSG_INSTANCE_RESET_FAILED`: `u32 reason`, `u32 mapId` (`0x49e54d`/`0x49e558`).
pub(super) fn read_instance_reset_failed(r: &mut &[u8]) -> io::Result<InstanceResetFailed> {
Ok(InstanceResetFailed {
reason: read_u32_le(r)?,
map: read_u32_le(r)?,
})
}
/// Read a body that is one bare `u32` — `SMSG_INSTANCE_RESET`'s map id (`0x49e481`),
/// `SMSG_UPDATE_LAST_INSTANCE`'s map id (`0x49e676`), `SMSG_UPDATE_INSTANCE_OWNERSHIP`'s flag
/// (`0x49e6c6`) and `SMSG_INSTANCE_SAVE_CREATED`'s flag (`0x4e7e6c`) are all exactly this.
pub(super) fn read_u32_body(r: &mut &[u8]) -> io::Result<u32> {
read_u32_le(r)
}
/// Body of `CMSG_RESET_INSTANCES` (VERIFIED both ways: the client's `ResetInstances` binding
/// `0x48a6b0` builds a `CDataStore` and sends opcode `0x31d` without writing a field; vmangos
/// `HandleResetInstancesOpcode` reads nothing): empty.
pub fn reset_instances() -> Vec<u8> {
Vec::new()
}