Files
benilla-octo/crates/benilla-protocol/src/messages/update_object/movement.rs
T
SamandClaude Fable 5 fd54ea9197 walkers keep walking, and dungeon entrances stop dropping you
- A creature that was already walking when it came into view stood frozen at
  its spawn point, then teleported — up to a full wander leg, 30 yards in the
  worst measured case — when its next movement packet arrived. The create
  packet carries the live path (where the unit is along it, how much is left)
  and we parsed all of it into the void. The unit now joins its walk exactly
  where the server has it: measured before/after, 13% of movement starts were
  over-a-yard teleports, now effectively zero.
- Walking into Zul'Gurub or Stratholme through the portal dropped you straight
  through the world. The landing check asks "is there a floor under me?" — but
  on the arrival frame the loaded world is still the map you just left, and an
  instance entrance is a copy of its outdoor ground at the same coordinates,
  0.23 yards under your feet. The old floor said yes, then despawned. The hold
  now waits for the destination's own ground to arrive, and a world that never
  arrives keeps the loading screen instead of dropping you into the void.
- Same fix covers a decoder hazard found on the way: a plain two-point path
  announces two points but ships only the destination, and trusting the count
  over-ran the packet — it never fires on our server's pathfinder, but now it
  can't.

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

232 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::io::{self, Read};
use crate::messages::movement::{
TransportPose, MOVEMENT_FLAG_JUMPING, MOVEMENT_FLAG_ON_TRANSPORT,
MOVEMENT_FLAG_SPLINE_ELEVATION, MOVEMENT_FLAG_SPLINE_ENABLED, MOVEMENT_FLAG_SWIMMING,
};
use crate::wire::{read_f32_le, read_packed_guid, read_u32_le, read_u64_le, read_u8, Vector3d};
/// Object class (the `TypeId` on a create packet).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ObjectType {
Object,
Item,
Container,
Unit,
Player,
GameObject,
DynamicObject,
Corpse,
}
impl ObjectType {
pub(super) fn from_u8(v: u8) -> Self {
match v {
1 => Self::Item,
2 => Self::Container,
3 => Self::Unit,
4 => Self::Player,
5 => Self::GameObject,
6 => Self::DynamicObject,
7 => Self::Corpse,
_ => Self::Object,
}
}
}
// MovementBlock update_flag bits (UpdateObject-only; the shared MOVEMENT_FLAG_* live in `super`).
const UPDATE_FLAG_TRANSPORT: u8 = 0x02;
const UPDATE_FLAG_MELEE_ATTACKING: u8 = 0x04;
const UPDATE_FLAG_HIGH_GUID: u8 = 0x08;
const UPDATE_FLAG_ALL: u8 = 0x10;
const UPDATE_FLAG_LIVING: u8 = 0x20;
const UPDATE_FLAG_HAS_POSITION: u8 = 0x40;
// SplineFlag final-destination bits (checked angle → target → point).
const SPLINE_FLAG_FINAL_POINT: u32 = 0x1_0000;
const SPLINE_FLAG_FINAL_TARGET: u32 = 0x2_0000;
const SPLINE_FLAG_FINAL_ANGLE: u32 = 0x4_0000;
/// `MoveSplineFlag::Flying` — and in vmangos 1.12 that bit alone *is* `Mask_CatmullRom`
/// (`MoveSplineFlag.h:48,77`), the same split [`crate::ServerPacket::MonsterMove`] reads: set ⇒ a 3-D
/// flight path that keeps its own Z, clear ⇒ a ground walk whose Z the client re-derives from terrain.
const SPLINE_FLAG_FLYING: u32 = 0x200;
/// `MoveSplineFlag::Cyclic` (`MoveSplineFlag.h:59`) — the path loops back on itself forever.
const SPLINE_FLAG_CYCLIC: u32 = 0x10_0000;
/// The **live spline a unit is already riding** when it streams into view — the `LIVING` block's
/// `MOVEFLAG_SPLINE_ENABLED` tail (vmangos `Object.cpp:535` → `PacketBuilder::WriteCreate`,
/// `packet_builder.cpp:152`). Without it, a creature that was walking when we first saw it stands
/// frozen at its create pose until the server's *next* `SMSG_MONSTER_MOVE` — which then teleports it
/// to wherever it actually got to (decision 0708).
///
/// [`Self::path`] is the **travel-order polyline**, the same shape `MonsterMove` decodes to. The wire
/// carries the server's raw internal control array (`MoveSpline::getPath()` = `Spline::getPoints()`),
/// which is *not* that: vmangos builds every spline — linear ones included — through
/// `SplineBase::InitCatmullRom` ("we should use catmullrom initializer even for linear mode!",
/// `spline.cpp:52`), so the array is `[phantom, p₀, …, pₙ, tail]` whose walkable range is
/// `index_lo = 1 ..= index_hi = len − 2` (`spline.cpp:238-270`): the phantom head is `p₀` extrapolated
/// one segment *backwards*, and the tail duplicates the destination (or, cyclic, closes the loop).
/// We hand over exactly that range.
#[derive(Debug, Clone)]
pub struct CreateSpline {
/// The full travel-order polyline in raw WoW coords: `[start, …waypoints…, endpoint]`.
pub path: Vec<[f32; 3]>,
/// The server's spline id (`MoveSpline::GetId`), as on `MonsterMove`.
pub id: u32,
/// Milliseconds of this spline the server has **already ridden** (`MoveSpline::timePassed`): the
/// unit is `time_passed_ms / duration_ms` of the way along `path`, so a client that starts the
/// ride at `now − time_passed_ms` lands exactly where the server has it.
pub time_passed_ms: u32,
/// The whole path's duration in ms (`MoveSpline::Duration` = the spline's length, which vmangos
/// initialises in *time* units at one constant velocity — `CommonInitializer`,
/// `MoveSpline.cpp:125-135`). Time is therefore uniform in arc length along the whole polyline,
/// which is what makes the `time_passed` fraction above exact.
pub duration_ms: u32,
/// `MoveSplineFlag::Flying`: a 3-D flight path (keep the spline's Z); clear for a ground walk.
pub flying: bool,
/// `MoveSplineFlag::Cyclic` — the server loops this path forever. The app rides it once; a looping
/// follow is unbuilt (the same gap `MonsterMove`'s cyclic paths have).
pub cyclic: bool,
}
/// The decoded fields a movement block carries: the pose (from its `LIVING`/`HAS_POSITION` flag),
/// for a `LIVING` block the unit's 6 movement speeds, its live [`CreateSpline`] and (if
/// `ON_TRANSPORT`) its rider pose, and (if `UPDATE_FLAG_TRANSPORT`) a transport GameObject's path
/// progress. The rest of the block is parsed and discarded to stay aligned.
pub struct MovementBlock {
pub position: Option<(Vector3d, f32)>,
/// A `LIVING` block's 6 movement speeds (yd/s) in wire order `[walk, run, run_back, swim, swim_back,
/// turn_rate]`; `None` for a `HAS_POSITION`-only block (GameObjects, which don't move under their
/// own power). The animation selector keys walk-vs-run on `walk` (boundary 2× it — RF-0057); the
/// net bridge extrapolates a remote mover between packets at `run`/`run_back`/`swim` + `turn_rate`.
pub speeds: Option<[f32; 6]>,
/// A transport GameObject's (`UPDATE_FLAG_TRANSPORT`, 0x02) path progress: the server's ms clock in
/// the path domain (vmangos `Object.cpp:590-605` — `GenericTransport::GetPathProgress()`, the same
/// `m_pathProgress` `ShipTransport::Update`/`CalculateSegmentPos` key off, `Transports/Transport.cpp:
/// 285-301`). `None` for every non-transport object: vmangos raises `UPDATEFLAG_TRANSPORT` only in
/// the transport GO paths — `ShipTransport`'s ctor (`Transport.cpp:39`) and the type-11 elevator
/// branch of `GameObject::Create` (`GameObject.cpp:246`); units/players never set it
/// (`Unit.cpp:88`). This is decision 0438's
/// **cycle anchor**: `anchor = this value`, `t₀ = Instant::now()` on create, `progress = anchor +
/// elapsed_ms` thereafter.
pub transport_progress: Option<u32>,
/// A `LIVING` block's rider pose on a transport (`MOVEFLAG_ON_TRANSPORT`, 0x0200_0000) — `Some` when this
/// unit/player is standing on a boat/zeppelin/elevator, its position local to that transport's frame
/// (decision 0438 "Riding is the mover's platform frame" — an observed rider carried this way
/// re-anchors through the transport's live matrix each frame, never treating `pos` as world-space).
pub transport: Option<TransportPose>,
/// The path this unit is **already walking** at create time (`MOVEFLAG_SPLINE_ENABLED`) — see
/// [`CreateSpline`]. `None` for a unit standing still, and for every non-`LIVING` block.
pub spline: Option<CreateSpline>,
}
impl MovementBlock {
pub(super) fn read(r: &mut impl Read) -> io::Result<Self> {
let update_flag = read_u8(r)?;
let mut position = None;
let mut speeds = None;
let mut transport = None;
let mut transport_progress = None;
let mut spline = None;
if update_flag & UPDATE_FLAG_LIVING != 0 {
let flags = read_u32_le(r)?;
let _timestamp = read_u32_le(r)?;
let living_position = Vector3d::read(r)?;
let living_orientation = read_f32_le(r)?;
position = Some((living_position, living_orientation));
if flags & MOVEMENT_FLAG_ON_TRANSPORT != 0 {
// A FULL u64, not packed: the LIVING block serializes through the same vmangos
// `MovementInfo::Write` as the MSG_MOVE relays (`Object.cpp:524` `*data << m`),
// whose `data << t_guid` is the plain-u64 `ObjectGuid` operator.
transport = Some(TransportPose {
guid: read_u64_le(r)?,
pos: Vector3d::read(r)?,
orientation: read_f32_le(r)?,
});
}
if flags & MOVEMENT_FLAG_SWIMMING != 0 {
let _pitch = read_f32_le(r)?;
}
let _fall_time = read_f32_le(r)?;
if flags & MOVEMENT_FLAG_JUMPING != 0 {
for _ in 0..4 {
let _ = read_f32_le(r)?; // z_speed, cos_angle, sin_angle, xy_speed
}
}
if flags & MOVEMENT_FLAG_SPLINE_ELEVATION != 0 {
let _ = read_f32_le(r)?;
}
// The 6 movement speeds (RF-0058 order): [0]=walk, [1]=run, [2]=run-back, [3]=swim,
// [4]=swim-back, [5]=turn-rate. The animation selector keys walk-vs-run on the walk speed
// (boundary = 2× it — RF-0057); the net bridge extrapolates a remote mover with the rest.
let mut s = [0.0f32; 6];
for slot in &mut s {
*slot = read_f32_le(r)?;
}
speeds = Some(s);
if flags & MOVEMENT_FLAG_SPLINE_ENABLED != 0 {
let spline_flags = read_u32_le(r)?;
// The dictated final facing, in vmangos's own angle → target → point order
// (`packet_builder.cpp:162-167`). Parsed for alignment only: a unit still riding a
// path faces its travel tangent every frame, exactly as `MonsterMove`'s facing snap is
// overwritten while a path runs (`net::apply::objects::monster_move`).
if spline_flags & SPLINE_FLAG_FINAL_ANGLE != 0 {
let _ = read_f32_le(r)?;
} else if spline_flags & SPLINE_FLAG_FINAL_TARGET != 0 {
let _ = read_u64_le(r)?;
} else if spline_flags & SPLINE_FLAG_FINAL_POINT != 0 {
let _ = Vector3d::read(r)?;
}
let time_passed_ms = read_u32_le(r)?;
let duration_ms = read_u32_le(r)?;
let id = read_u32_le(r)?;
let amount_of_nodes = read_u32_le(r)?;
let mut nodes = Vec::with_capacity(amount_of_nodes.min(0xFFFF) as usize);
for _ in 0..amount_of_nodes {
let v = Vector3d::read(r)?;
nodes.push([v.x, v.y, v.z]);
}
// The final destination trails the array; it is `path`'s last real point again
// (`MoveSpline::FinalDestination` = `getPoint(last())`), or zero on a cyclic path.
let _final_node = Vector3d::read(r)?;
// Drop the server's two virtual control points — see [`CreateSpline`]. Fewer than
// four nodes cannot hold a two-point path, so there is nothing to ride.
spline = (nodes.len() >= 4).then(|| CreateSpline {
path: nodes[1..nodes.len() - 1].to_vec(),
id,
time_passed_ms,
duration_ms,
flying: spline_flags & SPLINE_FLAG_FLYING != 0,
cyclic: spline_flags & SPLINE_FLAG_CYCLIC != 0,
});
}
} else if update_flag & UPDATE_FLAG_HAS_POSITION != 0 {
let pos = Vector3d::read(r)?;
let orientation = read_f32_le(r)?;
position = Some((pos, orientation));
}
if update_flag & UPDATE_FLAG_HIGH_GUID != 0 {
let _ = read_u32_le(r)?;
}
if update_flag & UPDATE_FLAG_ALL != 0 {
let _ = read_u32_le(r)?;
}
if update_flag & UPDATE_FLAG_MELEE_ATTACKING != 0 {
let _ = read_packed_guid(r)?;
}
if update_flag & UPDATE_FLAG_TRANSPORT != 0 {
transport_progress = Some(read_u32_le(r)?);
}
Ok(Self {
position,
speeds,
transport_progress,
transport,
spline,
})
}
}