creatures keep their first corner, and characters never share files

- a creature walking a path no longer cuts its first corner: the path's first waypoint was dropped at decode (2342)
- two characters whose names differ only in accented or non-Latin letters no longer share macros, bindings and SavedVariables; a file that failed to load is not overwritten at logout (2342)
- custom chat channels survive a logout, a collapsed quest header keeps its quests watched, and a trackpad swipe scrolls as far as it adds up to (2342)
- a hearth off a zeppelin no longer lands back on its deck, and an opened lockbox with loot left in it unlocks (2342)
- every snapshot runs on Vulkan, in a container, before it ships (2344)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam
2026-09-23 12:19:22 +07:00
co-authored by Claude Opus 5.5
parent 9c49f10882
commit 293b8e57fb
60 changed files with 1876 additions and 425 deletions
+4 -16
View File
@@ -560,6 +560,8 @@ pub(super) fn control(
if !player.server_riding {
movement_net::park_mover(&net.0 .0, &mut player);
}
// After the park, so a fear's ack carries the stopped word the park just reported.
movement_net::ack_speeds_undriven(&net.0 .0, &player, &speed_acks);
return;
}
// This frame's netted movement axes, the mouselook/turn modes they imply, and the autorun
@@ -1182,22 +1184,8 @@ pub(super) fn control(
// jump/fall lifecycle, and a ~500 ms heartbeat, each carrying the live `MovementInfo` (decisions
// 0052 + 0053). vmangos relays it to nearby players, who extrapolate from the flags. See the
// [`movement_net`] module (the outbound mirror of `net::motion`'s remote integration).
// The rider's local pose for the wire's ON_TRANSPORT tail: `bevy_to_wow` is a pure basis
// rotation, so the boat-local Bevy vector converts directly, and the local orientation is
// `face_yaw − boat_yaw` (the GetAbsoluteFacing law in reverse), normalized like any wire
// orientation.
let wire_transport = player.ride.as_ref().map(|r| {
let local = benilla_assets::coords::bevy_to_wow(r.local_pos);
benilla_protocol::TransportPose {
guid: r.guid,
pos: benilla_protocol::wire::Vector3d {
x: local[0],
y: local[1],
z: local[2],
},
orientation: (player.face_yaw - r.boat_yaw).rem_euclid(std::f32::consts::TAU),
}
});
// The rider's local pose for the wire's ON_TRANSPORT tail.
let wire_transport = movement_net::wire_transport(&player);
// The skipped-time clock (decision 1935): a held frame is a frame of movement simulation
// we advanced through without integrating, and the mover we name is the one we are
// actually driving — a possessed unit while we hold its reins. Read out before the call
@@ -580,6 +580,64 @@ pub(super) fn park_mover(sender: &Sender<ClientCommand>, player: &mut Player) {
player.last_facing = facing;
}
/// The rider's boat-local pose for the wire's `ON_TRANSPORT` tail, or `None` off a deck.
/// `bevy_to_wow` is a pure basis rotation, so the boat-local Bevy vector converts directly, and the
/// local orientation is `face_yaw − boat_yaw` (the GetAbsoluteFacing law in reverse), normalized
/// like any wire orientation.
pub(super) fn wire_transport(player: &Player) -> Option<TransportPose> {
player.ride.as_ref().map(|r| {
let local = bevy_to_wow(r.local_pos);
TransportPose {
guid: r.guid,
pos: benilla_protocol::wire::Vector3d {
x: local[0],
y: local[1],
z: local[2],
},
orientation: (player.face_yaw - r.boat_yaw).rem_euclid(std::f32::consts::TAU),
}
})
}
/// **The forced-speed acks owed on a frame the controller does not drive** — a server spline
/// (`server_riding`), a fear (`control_lost`) or a mover hand-off (`reseat`). Those frames return
/// before [`stream_self_movement`], which is where a controlled frame's acks go out, so until this
/// the change was applied locally and never answered: vmangos holds an unacked change for
/// `PendingAckResponseTime` (4 s) before enforcing it, counts it in `OnFailedToAckChange` (a kick
/// under the default anticheat penalty) and blocks the graveyard repop while one is pending. The
/// reference acks from its per-mover drain whoever is driving (`0x616142`/`0x61812d`).
///
/// The payload is the honest state we last reported: our streamed flags (a ride's deliberate
/// FORWARD included) with the transport tail when we are on a deck, minus the airborne pair — no
/// arc is being integrated on these frames, so there is no jump tail to send with them.
pub(super) fn ack_speeds_undriven(
sender: &Sender<ClientCommand>,
player: &Player,
acks: &[crate::net::SpeedChangeMessage],
) {
let transport = wire_transport(player);
let mut flags =
player.move_flags & OUTBOUND_FLAG_MASK & !(move_flags::FALLING | move_flags::FALLING_FAR);
if transport.is_none() {
flags &= !move_flags::ON_TRANSPORT; // flag and tail travel together
}
for ack in acks {
let _ = sender.send(ClientCommand::ForceSpeedAck {
kind: ack.kind,
guid: ack.guid,
counter: ack.counter,
speed: ack.speed,
flags,
pos: bevy_to_wow(player.pos),
orientation: player.face_yaw.rem_euclid(std::f32::consts::TAU),
pitch: 0.0,
fall_time: 0,
jump: None,
transport: transport.filter(|_| flags & move_flags::ON_TRANSPORT != 0),
});
}
}
#[cfg(test)]
mod tests {
/// A frame that integrated everything it was given — the skipped-time report's no-op input,
@@ -1440,6 +1498,73 @@ mod tests {
assert_eq!(player.move_flags, 0, "bookkeeping is zeroed after parking");
}
/// **A forced speed change is acked on a frame nobody is driving** — a fear, a server spline,
/// a mover hand-off. Those frames return before the stream, which is where a controlled
/// frame's acks go out, so the change was applied and never answered (vmangos enforces it 4 s
/// late, counts the miss toward its anticheat kick, and blocks the graveyard repop meanwhile).
/// The payload is the honest reported word: the ride's FORWARD kept, the airborne pair dropped
/// (no arc tail exists here), and `ON_TRANSPORT` only with the tail that must travel with it.
#[test]
fn a_speed_change_is_acked_while_not_driving() {
let (tx, rx) = crossbeam_channel::unbounded();
let ack = crate::net::SpeedChangeMessage {
guid: 7,
kind: benilla_protocol::SpeedKind::Run,
counter: 3,
speed: 3.5,
};
// A spline ride reporting FORWARD, off any deck, with a stale ON_TRANSPORT bit.
let player = Player {
move_flags: move_flags::FORWARD | move_flags::FALLING | move_flags::ON_TRANSPORT,
face_yaw: -1.0,
..Default::default()
};
ack_speeds_undriven(&tx, &player, &[ack]);
let Ok(ClientCommand::ForceSpeedAck {
counter,
speed,
flags,
orientation,
transport,
jump,
..
}) = rx.try_recv()
else {
panic!("the change is acked");
};
assert_eq!((counter, speed), (3, 3.5), "the ack echoes the change");
assert_eq!(
flags,
move_flags::FORWARD,
"FORWARD kept; FALLING and a tailless ON_TRANSPORT dropped"
);
assert!(transport.is_none() && jump.is_none());
assert!((0.0..TAU).contains(&orientation));
assert!(rx.try_recv().is_err(), "one ack per change");
// On a deck: the transport bit rides with its tail.
let player = Player {
move_flags: move_flags::ON_TRANSPORT,
ride: Some(super::super::state::PlayerRide {
entity: bevy::ecs::entity::Entity::PLACEHOLDER,
guid: 0x1F,
local_pos: bevy::math::Vec3::ZERO,
boat_yaw: 0.0,
}),
..Default::default()
};
ack_speeds_undriven(&tx, &player, &[ack]);
let Ok(ClientCommand::ForceSpeedAck {
flags, transport, ..
}) = rx.try_recv()
else {
panic!("the change is acked");
};
assert_eq!(flags, move_flags::ON_TRANSPORT);
assert_eq!(transport.map(|t| t.guid), Some(0x1F));
}
/// **A knockback launch acks, and sends no `MSG_MOVE_JUMP`** (decision 1702).
///
/// Two laws in one frame, and they are the same law read from both ends. `MSG_MOVE_JUMP` has a
+64 -17
View File
@@ -299,23 +299,7 @@ pub(super) fn apply_server_moves(
// Same-map teleport (the bridge only emits ours). Snap + echo the ack — without it the server
// freezes our movement until relog.
for t in teleports.read() {
player.pos = wow_to_bevy(t.position);
player.face_yaw = t.orientation;
cam.yaw = t.orientation;
// Stop any in-progress walk — server now sees us at the new spot.
player.move_flags = 0;
player.airborne_since = None; // a snap ends any in-progress jump arc (no phantom FALL_LAND)
player.settling = true; // hold (gravity off) until the destination's ground/buildings load
player.settle_since = time.elapsed_secs();
player.settle_deadline = time.elapsed_secs() + SETTLE_TIMEOUT;
// A far same-map teleport relocates us over ground that has not streamed either — the
// tiles we were standing on are still the resident ones. See [`Player::world_stale`].
player.world_stale = true;
// The relocation voids any in-progress self server-ride (the taxi flight-end teleport
// beats our own spline end by ~latency): `drive_self_ride` takes this flag next frame
// and drops the ride instead of mirroring the stale flight pose back over this snap
// (decision 0501 — the 4-yd hover + full-6s settle at every taxi landing).
player.ride_abort = true;
snap_near_teleport(player, &mut cam.yaw, t, time.elapsed_secs());
// The echo goes now, and it is the WHOLE near-teleport handshake (decision 1340). The
// real client echoes on the very next movement tick (wow-re: the 0xC7 drain applies the
// snap, then `0x60e0a0` sends guid+counter+time) and sends nothing else — no
@@ -523,6 +507,34 @@ pub(super) fn apply_server_moves(
speed_acks
}
/// **Apply one same-map teleport's snap** (`MSG_MOVE_TELEPORT_ACK` inbound) — everything but the
/// echo, which the caller sends. `cam_yaw` is the fly-cam's yaw, carried to the new facing.
fn snap_near_teleport(player: &mut Player, cam_yaw: &mut f32, t: &TeleportMessage, now: f32) {
// A near teleport always arrives detached: vmangos takes this path only with no `m_transport`
// (`Player::TeleportTo`, after `RemovePassenger`), and the reference's apply (`0x6186b0`)
// re-parents to the packet's transport guid — zero here. A ride left standing let this same
// frame's `ride::carry` compose the old deck pose back over the snap, and the airborne settle
// kept it attached, so a hearth from a zeppelin or a lift landed us back on the deck.
player.ride = None;
player.pos = wow_to_bevy(t.position);
player.face_yaw = t.orientation;
*cam_yaw = t.orientation;
// Stop any in-progress walk — server now sees us at the new spot.
player.move_flags = 0;
player.airborne_since = None; // a snap ends any in-progress jump arc (no phantom FALL_LAND)
player.settling = true; // hold (gravity off) until the destination's ground/buildings load
player.settle_since = now;
player.settle_deadline = now + SETTLE_TIMEOUT;
// A far same-map teleport relocates us over ground that has not streamed either — the tiles we
// were standing on are still the resident ones. See [`Player::world_stale`].
player.world_stale = true;
// The relocation voids any in-progress self server-ride (the taxi flight-end teleport beats our
// own spline end by ~latency): `drive_self_ride` takes this flag next frame and drops the ride
// instead of mirroring the stale flight pose back over this snap (decision 0501 — the 4-yd
// hover + full-6s settle at every taxi landing).
player.ride_abort = true;
}
/// Merge a server-authored packet's `MOVEMENTFLAGS` into our own — the reference's masked merge
/// (`0x618c30 @0x618deb`: `new = old ^ ((old ^ wire) & 0x75a07dff)`), not an assignment. Pure, so
/// the omission that actually bites is pinned by test: `ON_TRANSPORT` sits **outside** the mask, so
@@ -983,3 +995,38 @@ mod control_tests {
assert_eq!(control_verdict(ME, true, None), ControlVerdict::Granted(ME));
}
}
#[cfg(test)]
mod teleport_tests {
use super::*;
/// **A near teleport lands us where the server says, not back on the deck.** Hearthing off a
/// zeppelin or a lift is a same-map teleport, and vmangos removes the passenger before it
/// takes that path; the snap has to drop the ride too, or the same frame's platform carry
/// recomposes the old deck pose over it.
#[test]
fn a_near_teleport_leaves_the_transport() {
let mut player = Player {
active: true,
ride: Some(super::super::state::PlayerRide {
entity: Entity::PLACEHOLDER,
guid: 0x1F,
local_pos: Vec3::new(1.0, 0.0, 2.0),
boat_yaw: 0.5,
}),
..Default::default()
};
let mut cam_yaw = 0.0;
let t = TeleportMessage {
guid: 0x45,
counter: 1,
position: [-8_913.0, -136.0, 81.0],
orientation: 1.25,
};
snap_near_teleport(&mut player, &mut cam_yaw, &t, 10.0);
assert!(player.ride.is_none(), "the snap deboards");
assert_eq!(player.pos, wow_to_bevy(t.position));
assert_eq!((player.face_yaw, cam_yaw), (1.25, 1.25));
assert!(player.settling && player.ride_abort && player.world_stale);
}
}