- the music slot refuses everyone while a cinematic runs: the suppression was checked at two of the four callers, and the other two — the zone intro fanfare and SMSG_PLAY_MUSIC — played over the narration to their end, the fanfare even stamping its once-per-cooldown play under a shot you couldn't hear it through; the guard moved into the slot itself, the reference's own shape (its music pump dies at its own first instruction), and the narration now holds its own line in the voice budget, where before the client believed it had one more free voice than it did for the length of every cinematic - zone chat channels stop rejoining under a cinematic: the join walk reads the streamed area, which during a fly-by is wherever the camera is, so joins fired against zones the player was only looking at; the reference gates exactly this at two sites and rejoins once at the end — which our still-armed walk now does by simply falling through on the next frame - a lootable corpse sparkles: the reference watches the corpse's dynamic-flags loot bit and spawns its hard-coded "Loot Art" effect row — Particles\LootFX.m2 at the body's attachment point, no scale, colour or lifetime set, the asset's own emitters being the whole look — the same asset at the same tag our lootable dead units already wore, so both share one arm; the corpse ground decal, meanwhile, is (almost certainly) dead code; and the probes see a corpse as its own entity kind again — the death probe had been identifying it by a heuristic a refactor outdated, so it failed runs whose wire was entirely correct - the player controller splits along its concerns: one 1705-line function becomes six modules (the netted axes, posture, transport riding, the move-flag words, writing the frame onto the body, and the frame order itself), player.rs 2093 → 438 lines with zero behaviour change — checked by a source-line multiset and a live A/B of the same scripted key drive producing the identical opcode sequence and flags (walk 7.000 vs 6.978 yd/s, phase not physics) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
260 lines
14 KiB
Rust
260 lines
14 KiB
Rust
//! **This frame's decoded input**, in one place — the two things the controller derives from the
|
|
//! keyboard, the mouse and the binding table before any of it means anything to the avatar:
|
|
//!
|
|
//! - [`look_input`] — the both-button state and the reference's own **camera command word**
|
|
//! (1.12's `[InputControl+0x4]`, decision 1502), which the auto-follow is armed by. Read
|
|
//! *before* the look session, because both camera seats want it — including the one on the
|
|
//! not-driving path, which returns before the axes below are ever computed.
|
|
//! - [`move_axes`] — the **netted** forward/back and strafe axes plus the modes that select
|
|
//! between them (mouselook, keyboard turn), and the autorun latch with its cancel set. Netted
|
|
//! once here so that the direction we move, the speed we pick, the swim amounts and the flags we
|
|
//! stream can never disagree (decision 0056).
|
|
//!
|
|
//! Nothing here touches the avatar: `move_axes` writes exactly one field ([`Player::autorun`]),
|
|
//! because the autorun latch *is* input state.
|
|
|
|
use bevy::prelude::*;
|
|
|
|
use super::{camera, state, CameraControl, LookButton, Player};
|
|
|
|
/// What [`look_input`] read off the mouse and the bindings, before the look session runs.
|
|
pub(super) struct LookInput {
|
|
/// Vanilla's "both-button run" state (either real button pair, or MOVEANDSTEER).
|
|
pub both_buttons: bool,
|
|
/// The camera's input command word — see [`super::camera::follow_cmd`].
|
|
pub follow_command: u32,
|
|
}
|
|
|
|
/// The mouse/binding state the camera needs, built before the look session so the not-driving
|
|
/// path (which returns without ever reaching [`move_axes`]) can seat its camera from the same word.
|
|
pub(super) fn look_input(
|
|
buttons: &ButtonInput<MouseButton>,
|
|
binds: &crate::bindings::BindingsState,
|
|
player: &Player,
|
|
) -> LookInput {
|
|
// Both mouse buttons held together = vanilla's "both-button run": the avatar runs forward while
|
|
// the character steers with the mouse (turns like a right-drag), regardless of which button went
|
|
// down first. Checked directly here rather than through the controller's single-button look mode.
|
|
// MOVEANDSTEER (default Middle Mouse) is the same state through a binding — 1.12's own body
|
|
// runs the identical CameraOrSelectOrMove + TurnOrAction pair a both-button press does.
|
|
let steer_held = binds.pressed(crate::bindings::cmd::MOVE_AND_STEER);
|
|
let both_buttons =
|
|
(buttons.pressed(MouseButton::Left) && buttons.pressed(MouseButton::Right)) || steer_held;
|
|
|
|
// The camera's **input command word** (decision 1502) — 1.12's `[InputControl+0x4]`, bit for
|
|
// bit. The auto-follow is armed by *edges on this word* and its state is classified from it, so
|
|
// it is built once here, from the same binding state the movement code reads, and handed
|
|
// to both camera seats. MOVEANDSTEER sets both mouse bits because that is what the reference's
|
|
// binding does — it runs the identical CameraOrSelectOrMove + TurnOrAction pair.
|
|
let follow_command = {
|
|
use camera::follow_cmd as bit;
|
|
let mut w = 0;
|
|
let mut set = |on: bool, b: u32| {
|
|
if on {
|
|
w |= b;
|
|
}
|
|
};
|
|
set(
|
|
buttons.pressed(MouseButton::Right) || steer_held,
|
|
bit::RIGHT_MOUSE,
|
|
);
|
|
set(
|
|
buttons.pressed(MouseButton::Left) || steer_held,
|
|
bit::LEFT_MOUSE,
|
|
);
|
|
// `/follow` is the forward bit in the reference too — the same setter the W key drives
|
|
// (decision 0890), so it arms the camera exactly like a held W.
|
|
set(
|
|
binds.pressed(crate::bindings::cmd::MOVE_FORWARD) || player.follow_forward,
|
|
bit::FORWARD,
|
|
);
|
|
set(
|
|
binds.pressed(crate::bindings::cmd::MOVE_BACKWARD),
|
|
bit::BACKWARD,
|
|
);
|
|
set(
|
|
binds.pressed(crate::bindings::cmd::STRAFE_LEFT),
|
|
bit::STRAFE_LEFT,
|
|
);
|
|
set(
|
|
binds.pressed(crate::bindings::cmd::STRAFE_RIGHT),
|
|
bit::STRAFE_RIGHT,
|
|
);
|
|
set(
|
|
binds.pressed(crate::bindings::cmd::TURN_LEFT),
|
|
bit::TURN_LEFT,
|
|
);
|
|
set(
|
|
binds.pressed(crate::bindings::cmd::TURN_RIGHT),
|
|
bit::TURN_RIGHT,
|
|
);
|
|
set(player.autorun, bit::AUTORUN);
|
|
// The two externally-driven flags, which the reference folds into the camera's own
|
|
// Track/Fear bits rather than the input word — carried here so one word carries every edge.
|
|
set(player.server_riding, bit::TRACK);
|
|
set(player.control_lost, bit::FEAR);
|
|
w
|
|
};
|
|
|
|
LookInput {
|
|
both_buttons,
|
|
follow_command,
|
|
}
|
|
}
|
|
|
|
/// This frame's **netted** movement axes and the modes that shape them. Every forward/back,
|
|
/// strafe and turn consumer downstream reads these fields and not the keys.
|
|
#[derive(Clone, Copy)]
|
|
pub(super) struct MoveAxes {
|
|
/// Net forward/back: `+1` forward, `-1` back, `0` for a cancelled pair (see
|
|
/// [`state::forward_axis`]).
|
|
pub fwd: i32,
|
|
/// Net strafe, `+` right — Q/E always, A/D only while mouse-looking.
|
|
pub side: i32,
|
|
/// Right-mouse (or both-button) held: A/D strafe and the facing tracks the camera.
|
|
pub mouselook: bool,
|
|
/// A keyboard turn is being held — and is allowed (a stun kills it).
|
|
pub turning: bool,
|
|
/// The reference's `flags & 0xf`, off the *net* axes: W+S neither streams nor moves.
|
|
pub translating: bool,
|
|
/// Autorun was toggled **on** this frame — 0445's fifth cast-interrupt term, which the
|
|
/// flag delta cannot see.
|
|
pub autorun_armed: bool,
|
|
/// The raw strafe/turn keys, for the consumers that need the pressed state rather than the
|
|
/// net axis: the swim amounts and the wire's turn bits.
|
|
pub strafe_left: bool,
|
|
pub strafe_right: bool,
|
|
pub turn_left: bool,
|
|
pub turn_right: bool,
|
|
}
|
|
|
|
/// Decode the movement keys into [`MoveAxes`], running the autorun latch and its cancel set on
|
|
/// the way through.
|
|
pub(super) fn move_axes(
|
|
binds: &crate::bindings::BindingsState,
|
|
buttons: &ButtonInput<MouseButton>,
|
|
player: &mut Player,
|
|
rig: &CameraControl,
|
|
both_buttons: bool,
|
|
stunned: bool,
|
|
) -> MoveAxes {
|
|
// ── Autorun ── TOGGLEAUTORUN through the binding table (0997; 1.12 defaults NUMLOCK +
|
|
// BUTTON4 — the latter is winit's `Forward`, the thumb button this toggle lived on before
|
|
// the table existed, kept by the codec's BUTTON4 mapping). A latched mode, not a held key:
|
|
// the keyboard chord is typing-gated at dispatch like every binding, the mouse chord is
|
|
// not — and the reference agrees, its focus-loss handler releasing every direction bit
|
|
// while preserving `0x1000` (`0x514490`'s `and eax,0xfffff00f`, VERIFIED).
|
|
let mut autorun_armed = false;
|
|
if binds.fired(crate::bindings::cmd::TOGGLE_AUTORUN) {
|
|
player.autorun = !player.autorun;
|
|
autorun_armed = player.autorun;
|
|
}
|
|
// A mouse whose extra buttons don't land on Back/Forward would otherwise fail silently, and
|
|
// "nothing happened" is the least debuggable report there is. Name what did arrive.
|
|
for b in buttons.get_just_pressed() {
|
|
if let MouseButton::Other(n) = b {
|
|
info!("mouse: unmapped button Other({n}) — bindings know BUTTON4/BUTTON5 as winit Forward/Back");
|
|
}
|
|
}
|
|
// ── The cancel set ── autorun is NOT simply "held forward" — the thing that makes it its own
|
|
// mode is what *destroys* it. Six writers clear the bit in the reference; these are the ones
|
|
// with a benilla analog (VERIFIED, wow-re `rf79-autorun-cancel-set.md`):
|
|
//
|
|
// - **A W or S key-DOWN** — unconditional, and the subtle one: the directional handlers look
|
|
// pure (each pushes only its own bit), but they tail into the shared SET helper `0x514840`,
|
|
// which does `and [MOVE+4],0xffffefff` under `test cl,0x30` (fwd `0x10` | back `0x20`) at
|
|
// `0x514a5a`. A per-handler read answers "no" and is wrong about the behaviour. It runs
|
|
// *before* the axis (`0x5150a7` vs the emitter tail `0x5151a0`), so the axis never sees the
|
|
// combination. **Key-DOWN only**: the release path `0x514b70` restores nothing, which is why
|
|
// letting go of S after reversing leaves you standing rather than running again.
|
|
// - **The transition INTO both-buttons-held** (`0x514a73`, the same helper) — engaging the
|
|
// both-button run replaces autorun rather than stacking with it.
|
|
// - **Losing the mover** — death, root/stun, a taxi/charge hand-off. In the reference the
|
|
// emitter's gate `0x514560` goes down (health `<= 0`, `MOVEMENTFLAGS & 0x1200`, the on-taxi
|
|
// predicate) and writer #4 `0x514748` clears the bit as a side effect of the next emit; a
|
|
// level test is the faithful shape, not an edge. (Mechanism VERIFIED; the individual bit
|
|
// identities behind the gate are INFERRED — see the note's §4.)
|
|
//
|
|
// Deliberately absent, each VERIFIED as a *survivor*: a jump, a chat EditBox taking focus, and
|
|
// a zone change. Mounting is genuinely unsettled in the reference and left alone here.
|
|
let both_buttons_engaged = (both_buttons
|
|
&& (buttons.just_pressed(MouseButton::Left) || buttons.just_pressed(MouseButton::Right)))
|
|
|| binds.just_pressed(crate::bindings::cmd::MOVE_AND_STEER);
|
|
if state::autorun_cancelled(
|
|
binds.just_pressed(crate::bindings::cmd::MOVE_FORWARD),
|
|
binds.just_pressed(crate::bindings::cmd::MOVE_BACKWARD),
|
|
both_buttons_engaged,
|
|
player.modes.rooted || player.server_riding,
|
|
) {
|
|
player.autorun = false;
|
|
}
|
|
let autorun = player.autorun;
|
|
// ── The forward/back axis ── one net value ([`state::forward_axis`], whose tests pin the
|
|
// verified state table) read by every forward/back consumer downstream, so the direction we move,
|
|
// the speed we pick, the swim amounts and the flags we stream can't disagree (decision 0056).
|
|
//
|
|
// Zero is the state no "autorun = held forward" reading can produce, and it is reachable:
|
|
// hold S *first*, then toggle autorun — the toggle pushes X=`0x1000`, so `test cl,0x30` misses
|
|
// and the bit survives — and the client emits MSG_MOVE_STOP with S still held. The other order
|
|
// (autorun, then S) destroys the bit at key-down and walks you backward. Same two keys, two
|
|
// outcomes; that asymmetry is the whole shape of the feature.
|
|
// `/follow` enters as the FORWARD term, not a fifth source (decision 0890): the reference's
|
|
// follow pushes the very same move-forward bit `0x100000` the W key does, through the same
|
|
// setter, so it nets against a held S and diagonals with a strafe exactly like a held W. It
|
|
// rides the HELD state and not the key-DOWN edge, so it never trips the autorun cancel set
|
|
// above — which is right: synthesized input is not a keypress.
|
|
let fwd_axis = state::forward_axis(
|
|
binds.pressed(crate::bindings::cmd::MOVE_FORWARD) || player.follow_forward,
|
|
binds.pressed(crate::bindings::cmd::MOVE_BACKWARD),
|
|
both_buttons,
|
|
autorun,
|
|
);
|
|
// Vanilla turn/strafe control model (decision 0050, VERIFIED wow-5875-re `0x7c5360`): W/S move
|
|
// forward/back in the facing; **A/D turn the character** (rotate the facing at the turn rate) so
|
|
// the body faces where it runs — UNLESS right-mouse is held (mouse-look), where A/D strafe and
|
|
// the facing tracks the camera; **Q/E always strafe**. Movement basis is the *character* facing,
|
|
// so left-drag (camera-only orbit) doesn't change which way W walks.
|
|
let mouselook = both_buttons || rig.look == Some(LookButton::Right);
|
|
// The strafe axis, **netted exactly like `fwd_axis`** — Q/E always strafe, A/D only while
|
|
// mouse-looking. Netting is not a nicety: the two bits are mutually exclusive on the wire.
|
|
// Holding both keys used to OR `STRAFE_LEFT | STRAFE_RIGHT` into the flags while the avatar
|
|
// stood still (the controller's `dir` sum cancels), and vmangos **silently drops** every movement
|
|
// packet carrying that pair — never relaying it to anyone (decision 0622). Measured: 48 such
|
|
// packets in one session, 0 received by a watching client, against 0 in the reference
|
|
// client's entire 1.12.1 capture. That is decision 0056's invariant — the wire mirrors the
|
|
// avatar's actual motion — violated on this one axis only; the swim branch already nets.
|
|
let strafe_left = binds.pressed(crate::bindings::cmd::STRAFE_LEFT);
|
|
let strafe_right = binds.pressed(crate::bindings::cmd::STRAFE_RIGHT);
|
|
let turn_left = binds.pressed(crate::bindings::cmd::TURN_LEFT);
|
|
let turn_right = binds.pressed(crate::bindings::cmd::TURN_RIGHT);
|
|
let side_axis = i32::from(strafe_right) - i32::from(strafe_left)
|
|
+ if mouselook {
|
|
i32::from(turn_right) - i32::from(turn_left)
|
|
} else {
|
|
0
|
|
};
|
|
// TURNLEFT/TURNRIGHT turn the facing when not mouse-looking (yaw increases turning left,
|
|
// matching mouse-left). …and never while stunned: the reference skips the keyboard turn
|
|
// emitter `0x514f50` entirely (and force-stops an in-flight turn) behind the same
|
|
// `0x514755` gate. Killing the turn here is also what ends B179's *second* half — the walk
|
|
// animation a stunned character was still playing was the turn-in-place shuffle, which
|
|
// `gait` derives from real yaw change.
|
|
let turning = !mouselook && !stunned && (turn_left || turn_right);
|
|
// The net translate state — the reference's `flags & 0xf` (its four move bits), read off
|
|
// the *net* axes, not the keys: W+S streams no direction bit and doesn't translate.
|
|
let translating = fwd_axis != 0 || side_axis != 0;
|
|
|
|
MoveAxes {
|
|
fwd: fwd_axis,
|
|
side: side_axis,
|
|
mouselook,
|
|
turning,
|
|
translating,
|
|
autorun_armed,
|
|
strafe_left,
|
|
strafe_right,
|
|
turn_left,
|
|
turn_right,
|
|
}
|
|
}
|