Files
benilla-octo/crates/benilla-protocol/src/world/movement.rs
T
SamandClaude Fable 5 2ad0043523 benilla: initial public release
A from-scratch World of Warcraft 1.12.1 client in Rust + Bevy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 14:27:01 +07:00

69 lines
3.0 KiB
Rust

use crate::messages::MovementInfo;
use crate::wire::Vector3d;
/// `MovementFlags::FORWARD` — the only movement flag benilla sets.
pub(super) const MOVEMENT_FLAG_FORWARD: u32 = 0x1;
/// A monotonic **client-uptime** millisecond stamp for the `MovementInfo` time field — the kind the real
/// 1.12 client sends (`GetTickCount()`, ms since the client started), **not** wall-clock. vmangos reads it
/// into `MovementInfo::ctime` and uses it only two ways (VERIFIED, decision 0057): a non-zero guard
/// (`ctime == 0` reads as "not a client packet" and *pauses* its movement extrapolation) and *deltas*
/// (`getMSTimeDiff`, so the epoch cancels). It never compares the value to its own clock, and it
/// overwrites the field with its server clock before relaying to nearby players — so this value never
/// reaches observers. Hence: match the real client's clock kind, wrap cleanly as a full `u32` (~49.7 days,
/// which `getMSTimeDiff` handles), and keep it non-zero. (The old value was wall-clock masked to 31 bits,
/// which folded backwards every ~24.85 days — a delta glitch the clean `u32` wrap avoids.)
pub(super) fn client_uptime_ms() -> u32 {
use std::sync::OnceLock;
use std::time::Instant;
static START: OnceLock<Instant> = OnceLock::new();
(START.get_or_init(Instant::now).elapsed().as_millis() as u32).max(1)
}
/// Build a `MovementInfo` stamped with the client-uptime time ([`client_uptime_ms`]).
pub(super) fn movement_info(pos: [f32; 3], orientation: f32, flags: u32) -> MovementInfo {
MovementInfo {
flags,
timestamp: client_uptime_ms(),
position: Vector3d {
x: pos[0],
y: pos[1],
z: pos[2],
},
orientation,
// benilla never sets ON_TRANSPORT outbound yet (boarding/riding is decision 0438 phase 2) — the
// writer only emits the transport tail while the flag is set, so this is always dropped.
transport: None,
// Level swim (0) by default; the writer only emits the pitch tail while SWIMMING is set, and the
// controller supplies the live swim pitch through `send_movement` when it does.
pitch: 0.0,
fall_time: 0,
jump: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn client_uptime_ms_is_nonzero_and_monotonic() {
// The load-bearing property: vmangos treats `ctime == 0` as "not a client packet" and pauses its
// extrapolation, so our stamp must never be zero (the bug the old wall-clock value worked around).
// It must also be non-decreasing so `getMSTimeDiff` deltas stay sane.
let a = client_uptime_ms();
let b = client_uptime_ms();
assert!(a >= 1, "stamp is non-zero: {a}");
assert!(b >= a, "stamp is monotonic non-decreasing: {a} -> {b}");
}
#[test]
fn movement_info_is_stamped_nonzero() {
let info = movement_info([1.0, 2.0, 3.0], 0.5, MOVEMENT_FLAG_FORWARD);
assert_ne!(
info.timestamp, 0,
"a movement packet must carry a non-zero time"
);
}
}