Files
benilla-octo/crates/benilla-app/src/player/controller.rs
T
gromandClaude Opus 5 81d96af35d Everwood: the HD/OctoWoW line of benilla, imported onto upstream
This checkout had no git history at all, so this one commit carries the whole
of the Everwood work as a single import on top of chairmanpow/benilla-octo's
`main` (706cd71). The two trees have genuinely diverged — each has files the
other does not — so this is a fork point, not a catch-up.

Today's work, which is the reason a baseline exists at all:

- Booth previews no longer cast world shadows (`NO_BOOTH_SHADOW`). Portrait
  characters were entering the sun's shadow cascades, so every skinned-rig
  shadow specialization compiled live mid-frame; the hitch detector logged
  "main thread blocked 1316 ms" beside a `compiled LIVE` warning. This was
  the Fashion/Tmog stutter.
- Per-pane dress-up: each `DressUpModel` pane assembles and stages its own
  tried-on look instead of every pane mirroring the live player.
- Item template cache is append-only JSONL written on the IO pool, replacing
  an 11.6 MB whole-map JSON rewrite on the main thread every 10 s.
- `WOW_SYSTEM_TIMING` rebuilt per-thread: it took 2 mutexes per system enter
  and 4 per exit, costing 15 ms a frame (590 fps -> 60) and corrupting the
  measurements it was taken to produce.
- The visibility-churn sweep moved to its own `WOW_FLASH_CHURN` switch, so a
  frame-cost reading no longer pays 2.7 ms a frame for it.

Local state (`benilla-config/`, `logs/`, `target/`) is excluded by the
existing .gitignore; the account credential it holds is not in this commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-15 15:01:14 -06:00

800 lines
35 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.
//! The per-frame player controller, [`control`]: this frame's input to the avatar's pose, its
//! animation state and the packets owed the server. The phases live in sibling modules; their order
//! here is the reference's frame order and is load-bearing (the swim latch before the mover, the
//! posture commit before the pose, the ack after the launch).
use super::*;
/// Free-flies until the server places us, then drives the avatar and streams its movement as the
/// mover; the dev chord with `F` toggles free fly.
#[allow(clippy::type_complexity)]
pub(super) fn control(
time: Res<Time>,
keys: Res<ButtonInput<KeyCode>>,
buttons: Res<ButtonInput<MouseButton>>,
// Tupled parameters keep this system under Bevy's limit of 16.
pointer: (
Res<AccumulatedMouseMotion>,
Res<camera::LookConfig>,
Res<camera::ZoomLimit>,
Res<camera::FollowConfig>,
Res<camera_dynamics::CameraOptions>,
// Only `nearclip` is read here, for the self-avatar fade.
Res<benilla_world::view::ViewDistance>,
),
mut net: (
Res<NetCommands>,
MessageReader<TeleportMessage>,
MessageReader<WorldportMessage>,
MessageWriter<crate::creature_anim::SheathRequest>,
MessageReader<crate::net::SpeedChangeMessage>,
// Root, water walk, feather fall and hover on our mover, acked with the live pose.
MessageReader<crate::net::MoveModeMessage>,
// The hard-landing report (`0x602d00`: the wound vocal and the dust).
MessageWriter<crate::creature_anim::HardLanding>,
// The move edges the reference hands `AbortCast` (`0x6e4940`), for the cast bar.
ResMut<crate::spell::LocalMoveStart>,
// The mounted Space flourish plays locally at send; the net drain drops its echo.
MessageWriter<crate::creature_anim::MountFlourish>,
// A `MSG_MOVE_*` for our own mover: a pose the server wrote, no ack owed.
MessageReader<crate::net::SelfMoveMessage>,
MessageReader<StandStateRequest>,
// Control of a unit granted or revoked: the mover claim and parting pose are ours to give.
MessageReader<crate::net::ClientControlMessage>,
// A knockback on our mover: latched by `wire_in`, flown below, acked after the launch.
MessageReader<crate::net::KnockBackMessage>,
crate::tutorial::InputHooks,
// The loot window's move-start close (`0x60e990`).
ResMut<crate::ui_loot::LootMoveStart>,
// `SMSG_STANDSTATE_UPDATE`, applied locally and ungated, with nothing sent back.
MessageReader<super::ServerStandState>,
),
speed_capsule: (
Res<MoveSpeed>,
Res<PlayerCapsule>,
Res<InspectMode>,
Res<crate::ui_script::UiKeyboardCapture>,
Res<crate::ui_script::PlayerUiClickConsumed>,
// Every rebindable input reads here; raw `keys` serve only free fly and its chord.
Res<crate::bindings::BindingsState>,
// The far-sight subject, resolved earlier since this system holds our `Transform` mutably.
Res<view_subject::ViewSubject>,
// Tells a revoked body from a granted creature in the control handoff.
Res<crate::net::SelfGuid>,
Res<scoped_view::ScopedView>,
// The loading cover blanks input at the source; a gesture in flight is unwound below.
Res<crate::loading_screen::LoadingScreen>,
),
mut commands: Commands,
mut player: ResMut<Player>,
mut rig: ResMut<CameraControl>,
collide: benilla_world::collision::WorldCollision,
mut cameras: Query<(&mut Transform, &mut FlyCam), With<Camera>>,
// The body we drive, a possessed creature's included ([`BodyQuery`]).
mut body: BodyQuery,
window: Single<(&mut Window, &mut CursorOptions), With<PrimaryWindow>>,
// A press starts a look and arms a click test, which the release settles on time and travel.
mut world_clicks: (MessageWriter<WorldClick>, MessageWriter<WorldRightClick>),
mut click_test: (
Local<Option<camera::PressGesture>>,
Local<Option<camera::PressGesture>>,
),
// The liquid query, the transports, and the parent chain from a deck collider to its transport.
world_q: (
benilla_world::world_point::WorldPoint,
TransportQuery,
Query<&ChildOf>,
),
) {
let (world, transports, child_of) = (&world_q.0, &world_q.1, &world_q.2);
let (left_click, right_click) = (&mut *click_test.0, &mut *click_test.1);
let Ok((mut cam_t, mut cam)) = cameras.single_mut() else {
return;
};
let (mut window, mut cursor_opts) = window.into_inner();
let mouse_motion = &pointer.0;
let look_cfg = *pointer.1;
let zoom_max = pointer.2.max;
let (move_speed, capsule, inspect, ui_capture, click_consumed) = (
&speed_capsule.0,
&speed_capsule.1 .0,
&speed_capsule.2,
&speed_capsule.3,
&speed_capsule.4,
);
let binds = &speed_capsule.5;
let view_subject = &speed_capsule.6;
let self_guid = speed_capsule.7 .0;
let scoped = &speed_capsule.8;
let covered = speed_capsule.9.covering();
// Far sight turns the auto-follow off: our facing is not "behind" for a body we only watch.
let follow_cfg = camera::FollowConfig {
style: if view_subject.remote.is_some() {
camera::FollowStyle::Never
} else {
pointer.3.style
},
tracking_style: if view_subject.remote.is_some() {
camera::FollowStyle::Never
} else {
pointer.3.tracking_style
},
..*pointer.3
};
let dt = time.delta_secs();
// Free fly reads no keys while an EditBox is `typing` (not the per-key `consumed`, a binding's
// concern); `binds` applied that gate when it latched, and the dev chords ignore it.
let typing = ui_capture.typing;
// Every press below comes from `camera::latch_world_mouse`, so one the UI ate reaches none of
// them; the raw buttons only hold and release a gesture the world already owns. Read before
// the look session: the not-driving path seats its camera from this word and returns early.
let input::LookInput {
both_buttons,
follow_command,
} = input::look_input(binds, &player, &rig);
// Last frame's word, as `0x50fee0` reads it: its caller `0x514446` precedes the mover lookup.
let dynamics = camera_dynamics::DynamicsInput {
options: *pointer.4,
nearclip: pointer.5.nearclip,
// Last frame's cached surface, the reference's own lag.
surface_y: player.liquid_surface,
smooth_style: pointer.3.style,
tracking_style: pointer.3.tracking_style,
subject: camera_dynamics::SubjectState {
move_flags: player.move_flags,
// The driven body's descriptor: a possessed creature's taxi state, not ours.
taxi: body
.single()
.ok()
.and_then(|(_, _, _, _, _, store, ..)| store)
.is_some_and(|s| s.0.unit_flags() & 0x0010_0000 != 0),
track: player.server_riding,
fear: player.control_lost,
facing: player.face_yaw,
mounted: body
.single()
.ok()
.and_then(|(_, _, _, _, _, store, ..)| store)
.is_some_and(|s| s.0.unit_mount_display_id() > 0),
// `GetCurrentSpeed` (`0x7c4c90`) on the mover's speeds and last frame's word.
speed: body
.single()
.ok()
.and_then(|(_, _, _, _, _, _, _, speeds, ..)| speeds)
.map_or(0.0, |s| crate::net::current_speed(&s.0, player.move_flags)),
command: follow_command,
scoped: scoped.active(),
},
};
// A shadow copy, written back only on change: a `Mut` each frame marks `CursorOptions` changed,
// and bevy_winit then re-applies the cursor to the OS every frame, which can stall.
let mut opts_shadow = cursor_opts.bypass_change_detection().clone();
// The facing before the look session, restored below when the mouse may not turn the body.
let yaw_before_look = player.face_yaw;
// Off the driven body's descriptor. Stunned is `UNIT_FIELD_FLAGS & 0x40000` (`0x5145b0`),
// which skips the turn and pitch emitters (`0x514755`). Dead fails the shared precondition
// `0x5144e0` (health above zero), so a corpse is stunned to the input tick; a ghost has
// health 1. The stand state gates the mouse alone: `0x5145e0` wants the predicted
// `GetStandState() == 0` (`0x5ed570`; `stand_pending` is `[player+0x1d68]`).
let (stunned, dead, stand_byte) = body
.single()
.ok()
.and_then(|(.., store, _, _, _, _, _)| {
store.map(|s| {
(
s.0.unit_flags() & UNIT_FLAG_STUNNED != 0,
s.0.unit_is_dead(),
s.0.unit_stand_state(),
)
})
})
.unwrap_or((false, false, 0));
let stand_state = player.stand_pending.unwrap_or(stand_byte);
// The shared precondition `0x5144e0`, once per tick. Its far-sight conjunct,
// `!(IsActivePlayer(mover) && [mover+0x1c70] & 1)`, reads the resolved subject (`0x5ee290`
// latches only once resolved) and spares a possessed mover, which sets the same field.
let mover = state::MoverInput {
dead,
view_is_out: state::view_is_out(
player.foreign_mover.is_none(),
view_subject.remote.is_some(),
),
};
// `0x5145b0` now, for the mouse turn; its translate sibling waits for this frame's root edge.
let may_turn = mover.may_turn(stunned);
// `0x5145e0`: `may_turn` and standing; a turn key stands a seated body up instead.
let mouse_turns_body = mover.mouse_may_turn_body(stunned, stand_state);
// The drunk wobble, for the veer and the swim porpoise; zero when turning is refused, as the
// reference's sits behind `0x5145b0` (`0x60aa47`).
let drunk_wobble = {
let f = body
.single()
.ok()
.and_then(|(.., store, _, _, _, _, _)| store.and_then(|s| s.0.player_drunk_byte()))
.map_or(0.0, drunk::fraction);
if !may_turn {
0.0
} else {
drunk::wobble(time.elapsed().as_millis() as u32, f)
}
};
// A loading cover cancels a gesture in flight: ending the look session would settle its click
// test and click the world we just left, so the tests are dropped first.
if covered {
*left_click = None;
*right_click = None;
}
run_look_session(
&buttons,
mouse_motion,
both_buttons,
&mut rig,
&mut cam,
&mut player.face_yaw,
&mut window,
&mut opts_shadow,
inspect.enabled,
click_consumed.0,
&mut world_clicks.0,
&mut world_clicks.1,
left_click,
right_click,
look_cfg,
&dynamics,
time.elapsed_secs(),
);
// The mouse turns the view, but the body hand-off (`0x514474`) skips a body that is stunned,
// dead or seated (`0x5145e0`) or not ours to drive (`control_lost`, `reseat`), while the
// camera rotate (`0x514444`) still runs. The reference never writes the facing; writing and
// restoring is the same downstream. Standing up hands the camera's yaw back at the next
// motion sample, a snap, as `0x5103e0` commits the camera's facing rather than a delta.
if !mouse_turns_body || player.control_lost || player.reseat {
player.face_yaw = yaw_before_look;
}
{
let cur = cursor_opts.bypass_change_detection();
if cur.visible != opts_shadow.visible
|| cur.grab_mode != opts_shadow.grab_mode
|| cur.hit_test != opts_shadow.hit_test
{
*cursor_opts = opts_shadow;
}
}
// A rebound zoom key steps 1.0 per press, the stock `CameraZoomIn(1.0)` (`Bindings.xml:707`).
let zoom = binds.amount(crate::bindings::cmd::CAMERA_ZOOM_IN)
- binds.amount(crate::bindings::cmd::CAMERA_ZOOM_OUT);
apply_zoom_scroll(zoom, dt, &mut rig, zoom_max);
// Free fly is a dev tool on the dev chord: a bare `F` is the player's to bind.
if crate::run_mode::dev_chord(&keys, KeyCode::KeyF) {
player.detached = !player.detached;
}
// The server's movement edges and their acks. Speed changes come back unacked while we drive,
// for the movement stream to ack with the live pose.
let speed_acks = wire_in::apply_server_moves(
&time,
&mut commands,
&mut player,
&mut cam,
&net.0,
&mut net.1,
&mut net.2,
&mut net.4,
&mut net.5,
&mut net.12,
&mut net.9,
&mut net.11,
self_guid,
transports,
body.single()
.ok()
.map(|(_, t, ..)| (t.translation, server_ride::yaw_of(t.rotation))),
);
let flat = |v: Vec3| Vec3::new(v.x, 0.0, v.z).normalize_or_zero();
// On a transport, recompose the rider from the deck's pose this frame, before input integrates.
ride::carry(&mut player, &mut cam, transports);
if player.active && !player.detached {
// Not driving, while the camera stays on the body and input, physics and the stream yield:
// - `server_riding`: a server spline (Charge, knockback, taxi, a flee path) owns the body;
// `drive_self_ride` has mirrored it into `Player`, reporting FORWARD on purpose.
// - `control_lost`: someone else, or fear, drives our body. The server neither roots nor
// validates the victim, so this gate is the immobility.
// - `reseat`: the mover guid is changing and the claimed unit has not streamed in; moves
// carry no guid, so driving would write one body's pose onto another.
// Possession is none of these: it runs the ordinary path below, on the creature.
if player.server_riding || player.control_lost || player.reseat {
// The moving body's transform is the truth; without this the camera stays behind
// during a fear. Not while reseating, when `Player` still holds the body let go.
if player.control_lost && !player.reseat {
if let Ok((_, t, ..)) = body.single() {
let yaw = server_ride::yaw_of(t.rotation);
player.pos = t.translation;
player.face_yaw = yaw;
player.model_yaw = yaw;
}
}
let head = player.pos + Vec3::Y * (CAPSULE_HEIGHT - CAPSULE_RADIUS);
// Far sight outlives all three (Sentry Totem has no interrupt flags), and the
// auto-follow runs through the reference's `Track` and `Fear` states.
camera::seat_on_subject(
dt,
0.0,
player.pos,
head,
body.single().ok().and_then(|(_, _, _, pivot, .., net)| {
// The pivot preset follows the body's own SWIMMING (`0x50f880`), here the
// last streamed word, which `wire_in` merges from the server's poses.
body_pose::pivot_target(
pivot,
net,
player.move_flags() & crate::creature_anim::move_flags::SWIMMING != 0,
)
}),
view_subject,
&mut rig,
&mut cam,
&mut cam_t,
&collide,
&camera::FollowInput {
cfg: follow_cfg,
face_yaw: player.face_yaw,
command: follow_command,
},
&dynamics,
);
// Flush a stale run once, but never under a ride, whose FORWARD is deliberate.
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.
movement_net::ack_speeds_undriven(&net.0 .0, &player, &speed_acks);
return;
}
// `0x514560`, after `apply_server_moves`, so this frame's root edge is already in `modes`.
let may_translate = mover.may_translate(player.modes.rooted);
let axes = input::move_axes(
binds,
&buttons,
&mut player,
&rig,
both_buttons,
may_translate,
may_turn,
);
let input::MoveAxes {
fwd: fwd_axis,
side: side_axis,
mouselook,
turning,
translating,
autorun_armed,
turn_left,
turn_right,
..
} = axes;
if fwd_axis != 0 || side_axis != 0 {
net.13.moved();
}
if mouselook {
net.13.mouselooked();
}
// The driven unit's own speeds: the reference's input path never reads ours for another
// mover, so a possessed creature moves and turns at its own.
let mover_speeds = body.single().ok().and_then(|q| q.7).map(|s| s.0);
// Zero is the ctor state, not a rate: a unit whose create block has not landed falls back.
let turn_rate = mover_speeds
.map(|s| s.turn_rate)
.filter(|r| *r > 0.0)
.unwrap_or(TURN_RATE);
// This frame's own turn; `seat_camera` carries the camera by it, the two turning as one.
let mut turn_delta = 0.0;
if turning {
let mut turn = 0.0;
if turn_left {
turn += 1.0;
}
if turn_right {
turn -= 1.0;
}
// 0.75× while translating or falling (`flags & 0x200f`, `0x7c5c73`).
let slowed = translating || player.airborne_since.is_some();
let rate = turn_rate * if slowed { TURN_RATE_MOVING } else { 1.0 };
turn_delta = turn * rate * dt;
player.face_yaw += turn_delta;
}
// The drunk veer: while moving, the wobble adds to the facing every frame and commits
// through the facing setter, so it streams like a turn (`0x60aa70`-`0x60aab7`,
// `0x60de30`); a held keyboard turn skips it (`0x60aa5a`). Both yaw conventions grow
// leftward, and it joins `turn_delta`, so the camera follows the meander.
if drunk_wobble != 0.0 && translating && !turning {
player.face_yaw += drunk_wobble;
turn_delta = drunk_wobble;
}
let face_rot = Quat::from_rotation_y(player.face_yaw);
let move_fwd = flat(face_rot * Vec3::NEG_Z);
let move_right = flat(face_rot * Vec3::X);
let mut dir = Vec3::ZERO;
// One step in the net axis's sign, as the emitter issues one START in `sign(axis)`.
match fwd_axis.signum() {
1 => dir += move_fwd,
-1 => dir -= move_fwd,
_ => {}
}
// Strafe likewise; a cancelled pair is no strafe.
match side_axis.signum() {
1 => dir += move_right,
-1 => dir -= move_right,
_ => {}
}
// Translation dies here, but a root leaves turning live: the reference's allow-list permits
// the turn, pitch, run/walk and SetFacing commands (`0x615c71`, `0x618054`). A stun, which
// vmangos applies with a root (`SpellAuras.cpp:3502`), or death stops the turn as well.
if !may_translate {
dir = Vec3::ZERO;
}
let moving = dir != Vec3::ZERO;
// A keyboard turn or the veer stands a seated body; a mouse turn does not, since the
// facing commit refuses a seated player (`0x51460c`, `0x51520a`) and the turn emitter
// skips its stand arm while the right button is held (`0x514f6d`). A right-click stands
// you instead (`0x514ae0`, `camera::PressGesture::is_click`; `crate::target::click`).
let turned = turn_delta != 0.0;
// `TOGGLERUN` before the speed select, which reads the bit this frame's press left: the
// mover reads `CMovement+0x40` after the input phase.
walk::update(&mut player, &body, binds);
// The stand state and the sheath toggle, which interlock; returns the committed stand.
let stand_now = posture::update(
&mut player,
&body,
binds,
&net.0,
&mut net.3,
&mut net.10,
&mut net.15,
moving,
turned,
);
// The backward flag selects the backward speed over strafe; a backward jump lands shorter.
let net_backward = fwd_axis < 0;
// The server's speeds (the LIVING block, then `SMSG_FORCE_*_SPEED_CHANGE`), or before the
// create and under the `WOW_MOVE_SPEED` override, a set at the 2.5/4.5/7.0 ratios.
let speeds = match mover_speeds {
Some(s) if !move_speed.env_override => s,
_ => benilla_protocol::MoveSpeeds {
walk: move_speed.value * WALK_RATIO,
run: move_speed.value,
run_back: move_speed.value * RUN_BACK_RATIO,
..Default::default()
},
};
// `GetCurrentSpeed` (`0x7c4c90`), shared with the remote extrapolator: the walk arm comes
// before the backward min, so walking backwards is walk speed. It takes this frame's gait
// intent, since the wire word is built after the mover.
let speed = crate::net::current_speed(
&speeds,
if net_backward {
move_flags::BACKWARD
} else {
move_flags::FORWARD
} | if player.walking {
move_flags::WALK_MODE
} else {
0
},
);
// `Jump` (`0x513bd0`) inlines `0x5144e0` and `0x514560`, which is `may_translate` term for
// term: health, root and stand state 7. Hover's refusal is the movement handler's
// (`0x7c623a`), which keeps the mounted flourish reachable while hovering.
let mut want_jump = binds.fired(crate::bindings::cmd::JUMP) && may_translate;
// Swim or walk, latched with hysteresis at the `0x6030c0` boundary against flicker.
let surface_y = swim::surface_over_feet(world, player.pos);
// For the camera's water corridor, which reads it a frame late, as `0x670630` does.
player.liquid_surface = surface_y;
let swimming = swim::update_swimming(&mut player, surface_y, time.elapsed_secs());
if let Some(surface) = surface_y {
move_trace::swim(player.pos.y, surface, swimming, player.collision_height.0);
}
// Space while swimming is the Jump command (`0x7c6230`), once per press: it breaches at
// the surface and hops about 1.6 yd below it, re-latching swim once the launch halves
// (`0x7c5de0`). It clears SWIMMING before the mover, so this frame's mover, flags and wire
// see a jump. Hover refuses it (`0x7c623a`, ahead of the take-off select at `0x7c6261`),
// and hover does not stop swim entry (`0x6030c0` tests only LEVITATING).
//
// The `Jump(force = 0)` a hover grant owes: `force` 0 skips the hover refusal (`0x7c6236`),
// so it launches a body already hovering; root and falling still refuse it (`0x7c625c`).
let wire_jump = player.take_wire_jump();
// The knockback: horizontal `(cos, sin) · xy_speed` in absolute world XY, vertical
// `−zspeed`, since the wire's take-off speed is down-positive, as in the jump tail.
let knockback = player.take_knockback();
let knock_launch = knockback.map(|k| {
let (c, sn, xy) = (k.launch.cos_angle, k.launch.sin_angle, k.launch.xy_speed);
wow_to_bevy([c * xy, sn * xy, -k.launch.zspeed])
});
// A knockback sets FALLING, which excludes SWIMMING; it brings its own launch, so the land
// mover flies it, not `breach_step`.
let breach = swimming && (want_jump && !player.modes.hover || wire_jump);
let knock_breach = swimming && knock_launch.is_some();
if breach || knock_breach {
player.swimming = false;
}
let swimming = swimming && !breach && !knock_breach;
// Read by both the swim mover and the flag build, so the two cannot disagree.
let (swim_fwd, swim_side) = if swimming {
swim::translate_amounts(&axes, !may_translate)
} else {
(0.0, 0.0)
};
// The mounted Space flourish, the jump-key handler `0x60dea0`: mounted, still and grounded
// plays MountSpecial (94) locally, then sends `CMSG_MOUNTSPECIAL_ANIM`; moving jumps;
// turning in place eats the press. Airborne, the press falls through and the mover drops
// it (the reference's ground-clearance test `0x605650`; the airborne arc stands in).
if want_jump && !moving && !swimming && player.airborne_since.is_none() {
if let Ok((e, .., store, _, _, _, _, _)) = body.single() {
if store.is_some_and(|s| s.0.unit_mount_display_id() != 0) {
want_jump = false;
if !turning {
let _ = net.0 .0.send(ClientCommand::MountSpecial);
net.8.write(crate::creature_anim::MountFlourish { unit: e });
}
}
}
}
// The mover pitch (`CMovement+0x20`), in every mode: held when unsteered (`0x7c4f80`) and
// set by mouse look to the aim through `SetPitch` (`0x7c6f70`), an unconditional store
// clamped ±89° (the ±π/2 clamp is the pitch keys'), whose write precedes its SWIMMING test
// (`0x7c6f91`). A left-drag orbit steers nothing. It is written per mouse motion (event
// `0x400500cb`), not per frame, so other writers survive a still mouse. The aim includes
// the pivot bias, as `0x5103e0` passes `[cam+0x104] + [cam+0xf4]`.
let aim_pitch = cam.pitch + rig.smart_pivot.bias();
if mouselook && aim_pitch != player.aim_pitch_seen {
player.aim_pitch_seen = aim_pitch;
player.mover_pitch = aim_pitch.clamp(-MOUSELOOK_PITCH_CLAMP, MOUSELOOK_PITCH_CLAMP);
}
let mut swim_pitch = 0.0_f32;
// Pre-step feet Y, the true take-off height: the step already rises one jump tick.
let launch_y = player.pos.y;
let mover::Outcome {
held,
grounded,
jumped,
knocked,
air_nudged,
ground,
} = if breach {
// Jump while swimming (`0x7c6230`): falls unconditionally, seeded about 14% over a land
// jump, and streams as a normal jump.
swim::breach_step(&mut player, &time, &collide, capsule)
} else if swimming {
// One swimming frame; it also picks the presented pitch, levelled at the surface cap.
let frame = swim::drive_step(
&mut player,
&time,
&collide,
capsule,
world,
surface_y,
(move_fwd, move_right),
(swim_fwd, swim_side),
mover_speeds,
move_speed,
if translating { drunk_wobble } else { 0.0 },
);
swim_pitch = frame.pitch;
frame.outcome
} else {
// Water walking hands the mover the liquid surface as ground ([`mover::water_floor`]),
// where the reference ORs the liquid layers into the walk trace's class mask
// (`0x63162e`); liquid is queried here rather than swept.
let water_floor = mover::water_floor(
player.modes.water_walking,
swimming,
player.mover_pitch,
surface_y,
);
mover::step(
&mut player,
&time,
&collide,
capsule,
moving,
dir,
speed,
want_jump,
wire_jump,
knock_launch,
water_floor,
// The air nudge speed is `min(walk, run)`, live (`0x7c4c90(1)`, `0x7c4d19`), with
// the default walk speed until the server sends speeds.
mover_speeds.map_or(super::AIR_NUDGE_SPEED, |s| s.walk.min(s.run)),
)
};
// Traced here, the first point that knows both the latch and the mover's verdict.
if let Some(k) = knockback {
move_trace::knockback(knocked, k.launch);
}
let now = time.elapsed_secs();
// Our FALLING: never while swimming, and a root ends it outright (`SetRoot` `0x7c7340`
// calls `StopFalling` `0x7c6290`), leaving the body hanging; `grounded` keeps true contact.
let airborne = !swimming && !held && !player.modes.rooted && (!grounded || jumped);
// Airborne keeps the attachment, so a jump above a deck lands where it took off.
ride::update_attachment(
&mut player,
transports,
child_of,
ground,
grounded,
swimming,
);
// The live flag word, the take-off-frozen one the animation reads, and the fall clock.
let flags::FrameFlags {
wire: move_flags_now,
pose: pose_flags,
landed,
fall_time: wire_fall_time,
} = flags::this_frame(
&mut player,
&axes,
swimming.then_some((swim_fwd, swim_side)),
airborne,
jumped,
held,
air_nudged,
may_translate,
may_turn,
now,
launch_y,
);
let anim_flags = gait::drive_body_heading(
&mut player,
pose_flags,
dt,
swimming,
moving,
airborne,
turning || mouselook,
turn_rate,
);
let cam_pivot_target = body_pose::drive(
&player,
&mut body,
&mut net.6,
swimming,
swim_pitch,
move_flags_now,
anim_flags,
landed,
stand_now,
);
// The camera sweep's root: the capsule's top hemisphere centre, not the framing pivot.
let head = player.pos + Vec3::Y * (CAPSULE_HEIGHT - CAPSULE_RADIUS);
// The spyglass lock (aura 76): re-parking each frame stands in for the reference's camera
// flag `0x8`, which makes `SetCameraView` return early.
if scoped.active() {
rig.park_distance(0.0);
}
// Far sight moves only the picture: `seat_on_subject` orbits the object, all else runs.
// `WOW_CAM_DUMP`: this frame's input beside `seat_camera`'s realized `[cam]` line.
if crate::player::camera::cam_dump_enabled() {
eprintln!(
"[turn] t={:.6} dt={:.6} dx={:.3} dy={:.3} look={} face={:.6} model={:.6} \
pos [{:.4},{:.4},{:.4}] pivot={:.4}->{:.4}",
time.elapsed_secs_f64(),
dt,
mouse_motion.delta.x,
mouse_motion.delta.y,
match rig.look {
Some(LookButton::Right) => "R",
Some(LookButton::Left) => "L",
None => "-",
},
player.face_yaw,
player.model_yaw,
player.pos.x,
player.pos.y,
player.pos.z,
// The pivot channel's live height and its target.
rig.pivot.probe().0,
rig.pivot.probe().1,
);
}
// `turn_delta` is our own turn; the deck's yaw reached `cam.yaw` in the ride carry.
let follow = camera::FollowInput {
cfg: follow_cfg,
face_yaw: player.face_yaw,
command: follow_command,
};
camera::seat_on_subject(
dt,
turn_delta,
player.pos,
head,
cam_pivot_target,
view_subject,
&mut rig,
&mut cam,
&mut cam_t,
&collide,
&follow,
&dynamics,
);
// The cast bar's self-cancel: a new directional start, a jump, or autorun's on edge (the
// interrupt mask `0x10f0` at `0x5150ce` is forward, back, strafe and autorun, and a clear
// edge returns early at `0x5150c8`); turning and pitch never cancel. Only our own
// character's moves count: vmangos breaks a channel on the caster's own movement
// (`Spell::update`), so a possessed creature's steps must not end Mind Control.
let steering_ourselves = player.foreign_mover.is_none();
if steering_ourselves
&& (move_flags_now & move_flags::ANY_MOVE & !player.move_flags != 0
|| jumped
|| autorun_armed)
{
net.7 .0 = true;
}
// The loot window's walk-away, the movement-start guard `0x60e990`: forward, back, strafe,
// keyboard turn, pitch and jump close it, mouse-look facing does not, and only for our
// own character.
if steering_ourselves
&& (move_flags_now
& (move_flags::ANY_MOVE | move_flags::TURN_LEFT | move_flags::TURN_RIGHT)
& !player.move_flags
!= 0
|| jumped)
{
net.14 .0 = true;
}
// Stream the movement: a `MSG_MOVE_*` per axis transition, the jump and fall edges and a
// 500 ms heartbeat, which vmangos relays to nearby players.
let wire_transport = movement_net::wire_transport(&player);
// A held frame is simulation time skipped; the mover named is the one we drive.
let skip = movement_net::SkipClock {
dt,
held: player.settling,
mover: player.foreign_mover.or(self_guid),
};
movement_net::stream_self_movement(
&net.0 .0,
&mut player,
move_flags_now,
swim_pitch,
movement_net::ArcEdges {
jumped,
// Wire launches send no `MSG_MOVE_JUMP`: the hover grant's jump sends nothing
// (`0x61a620`) and a knockback sends its ack instead.
wire_launch: knocked || wire_jump,
air_nudged,
landed,
fall_time: wire_fall_time,
},
now,
&speed_acks,
// Acked only if flown: under root the reference drops the record unacked (`0x615c71`).
knockback.filter(|_| knocked),
wire_transport,
skip,
);
} else {
// Free fly. Park the mover first, so a detach mid-move leaves no stale flags for observers
// to extrapolate; a no-op once stopped.
movement_net::park_mover(&net.0 .0, &mut player);
camera::fly_free(dt, &keys, typing, &mut rig, &mut cam, &mut cam_t);
}
}