a possessed body is driven, not merely held
- the active mover is a marker on the body in your hands: a mind-controlled creature drives with its own speeds and turn rate, and your identity never follows it - a control packet is about that body, not about you — a fear on the thing you drive no longer tears the possession down - your own turn rate reads live mover state, so speed and turn auras it previously ignored now apply Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -50,7 +50,7 @@ use bevy::ecs::entity::{EntityHashMap, EntityHashSet};
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::creature_anim::AnimData;
|
||||
use crate::net::{NetEntity, SelfPlayer};
|
||||
use crate::net::{ActiveMover, NetEntity};
|
||||
use crate::player::CameraControl;
|
||||
use benilla_world::decal::{DecalFrame, WorldDecal};
|
||||
use benilla_world::model_fade::{fade_alpha, RenderFade};
|
||||
@@ -184,7 +184,7 @@ fn update_shadows(
|
||||
(
|
||||
&Transform,
|
||||
&ModelAnimations,
|
||||
Has<SelfPlayer>,
|
||||
Has<ActiveMover>,
|
||||
Option<&crate::entities::mount::MountChild>,
|
||||
),
|
||||
Without<BlobShadow>,
|
||||
|
||||
@@ -61,7 +61,7 @@ use benilla_ui::script::{pieces, Backdrop, Insets};
|
||||
use benilla_ui::script::{JustifyH, JustifyV, Outline};
|
||||
|
||||
use crate::entities::{overhead_anchor, BoneAttach, OverheadFallback};
|
||||
use crate::net::{Guid, NetEntity, SelfGuid, SelfPlayer};
|
||||
use crate::net::{ActiveMover, Guid, NetEntity, SelfGuid};
|
||||
use crate::ui_chat::{default_color, ChatEventKind};
|
||||
use crate::ui_pass::{UiQuad, UiQuadAppend, UiQuads, UvRect};
|
||||
use crate::ui_text::{layout_text_quads, measure_text, FontSpec, Justify, UiFontAtlas};
|
||||
@@ -300,7 +300,7 @@ fn drive_bubbles(
|
||||
vplates: Res<VPlates>,
|
||||
self_guid: Res<SelfGuid>,
|
||||
units: Query<(Entity, &Guid, &Transform), With<NetEntity>>,
|
||||
self_q: Query<&Transform, With<SelfPlayer>>,
|
||||
self_q: Query<&Transform, With<ActiveMover>>,
|
||||
camera: Query<(&Camera, &Transform), With<WorldCamera>>,
|
||||
mut atlas: Option<ResMut<UiFontAtlas>>,
|
||||
mut quads: ResMut<UiQuads>,
|
||||
|
||||
@@ -53,7 +53,7 @@ use bevy::prelude::*;
|
||||
use benilla_assets::{WmoGroupNav, WmoModel, WmoPortalRef};
|
||||
|
||||
use crate::entities::mount::MountBody;
|
||||
use crate::net::SelfPlayer;
|
||||
use crate::net::ActiveMover;
|
||||
use crate::target::SelectionRadius;
|
||||
use benilla_world::view::WorldCamera;
|
||||
use benilla_world::wmo_portal::{UnitWmoRoom, WmoPortalInstance, WmoRoom};
|
||||
@@ -77,7 +77,7 @@ const FALLBACK_RADIUS: f32 = 6.0;
|
||||
/// portal-PVS room test (decision 0739 — see the module doc's "room leg"). PostUpdate,
|
||||
/// before [`AnimationSystems`] — a wake drops the marker in time for the same frame's pose
|
||||
/// evaluation, so the re-appearing unit samples the absolute-clock pose with no stale
|
||||
/// frame. Exempt: the self-avatar (the camera rides its attachment-17 pivot, and it must keep
|
||||
/// frame. Exempt: the body we drive (the camera rides its attachment-17 pivot, and it must keep
|
||||
/// animating faded-out in first person) and its mount child. `WOW_NO_ANIM_LOD=1` disables
|
||||
/// parking — the live-probe A/B lever; `WOW_NO_ROOM_LOD=1` disables only the room leg.
|
||||
#[allow(clippy::type_complexity, clippy::too_many_arguments)] // one Bevy system's full input set
|
||||
@@ -90,7 +90,7 @@ pub(super) fn gate_rig_animation(
|
||||
&GlobalTransform,
|
||||
Option<&SelectionRadius>,
|
||||
Has<AnimParked>,
|
||||
Has<SelfPlayer>,
|
||||
Has<ActiveMover>,
|
||||
Option<&MountBody>,
|
||||
Option<&UnitWmoRoom>,
|
||||
),
|
||||
@@ -104,7 +104,7 @@ pub(super) fn gate_rig_animation(
|
||||
// `RigPose`), so this query can never park the character pane.
|
||||
With<RigPose>,
|
||||
>,
|
||||
self_hosts: Query<Has<SelfPlayer>>,
|
||||
self_hosts: Query<Has<ActiveMover>>,
|
||||
instances: Query<&WmoPortalInstance>,
|
||||
wmos: Res<Assets<WmoModel>>,
|
||||
mut out_since: Local<EntityHashMap<f32>>,
|
||||
@@ -616,11 +616,11 @@ mod tests {
|
||||
let mut app = app();
|
||||
spawn_camera(&mut app);
|
||||
let (behind, behind_joint) = spawn_rig(&mut app, Vec3::Z * 50.0);
|
||||
app.world_mut().entity_mut(behind).insert(SelfPlayer);
|
||||
app.world_mut().entity_mut(behind).insert(ActiveMover);
|
||||
advance(&mut app, PARK_AFTER_SECS + 0.2, 4);
|
||||
assert!(
|
||||
!app.world().entity(behind).contains::<AnimParked>(),
|
||||
"SelfPlayer never parks, wherever the camera points"
|
||||
"the active mover never parks, wherever the camera points"
|
||||
);
|
||||
// And its bones keep animating — the exemption is live evaluation, not just the marker.
|
||||
let before = bone(&app, behind_joint);
|
||||
|
||||
@@ -448,7 +448,7 @@ fn publish_world_units(
|
||||
bodies: Query<WireBody>,
|
||||
viewers: Query<(
|
||||
Entity,
|
||||
Has<crate::net::SelfPlayer>,
|
||||
Has<crate::net::ActiveMover>,
|
||||
Has<benilla_world::world_unit::ViewerUnit>,
|
||||
)>,
|
||||
) {
|
||||
@@ -1117,7 +1117,7 @@ mod world_unit_tests {
|
||||
.world_mut()
|
||||
.spawn((
|
||||
net(EntityKind::Player, 1.0),
|
||||
crate::net::SelfPlayer,
|
||||
crate::net::ActiveMover,
|
||||
collision_height::CollisionHeight(2.5),
|
||||
))
|
||||
.id();
|
||||
@@ -1164,14 +1164,14 @@ mod world_unit_tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// The viewer marker is reconciled, not stamped once: a `/logout` takes `SelfPlayer` away and
|
||||
/// The viewer marker is reconciled, not stamped once: a `/logout` takes `ActiveMover` away and
|
||||
/// the marker has to go with it, or the next character is feathered as someone else's avatar.
|
||||
#[test]
|
||||
fn the_viewer_marker_follows_self_player_off_as_well_as_on() {
|
||||
let mut app = App::new();
|
||||
let me = app
|
||||
.world_mut()
|
||||
.spawn((net(EntityKind::Player, 1.0), crate::net::SelfPlayer))
|
||||
.spawn((net(EntityKind::Player, 1.0), crate::net::ActiveMover))
|
||||
.id();
|
||||
app.world_mut()
|
||||
.run_system_once(publish_world_units)
|
||||
@@ -1183,7 +1183,7 @@ mod world_unit_tests {
|
||||
|
||||
app.world_mut()
|
||||
.entity_mut(me)
|
||||
.remove::<crate::net::SelfPlayer>();
|
||||
.remove::<crate::net::ActiveMover>();
|
||||
app.world_mut()
|
||||
.run_system_once(publish_world_units)
|
||||
.expect("reconciler runs");
|
||||
|
||||
@@ -49,7 +49,7 @@ use bevy::prelude::*;
|
||||
|
||||
use crate::creature_anim::{footfall_side, move_flags, AnimSoundEvent, MovementState};
|
||||
use crate::entities::{BoneAttach, Creatures};
|
||||
use crate::net::{NetEntity, ObjectStore, SelfPlayer};
|
||||
use crate::net::{ActiveMover, NetEntity, ObjectStore};
|
||||
use crate::sound::footsteps::Footsteps;
|
||||
use benilla_assets::{AssetSet, LockRecover, WorldAssets};
|
||||
use benilla_world::decal::{DecalFrame, WorldDecal};
|
||||
@@ -149,7 +149,7 @@ fn load_ink(
|
||||
|
||||
/// The spawner's ROOT-unit reads (see [`spawn_footprints`]'s `roots` param).
|
||||
type RootState = (
|
||||
Has<SelfPlayer>,
|
||||
Has<ActiveMover>,
|
||||
Option<&'static NetEntity>,
|
||||
Option<&'static ObjectStore>,
|
||||
Option<&'static MovementState>,
|
||||
@@ -169,7 +169,7 @@ fn spawn_footprints(
|
||||
// rider's SCALE_X — RE-corrected), and the state gates all read the root.
|
||||
parents: Query<&ChildOf>,
|
||||
roots: Query<RootState>,
|
||||
self_pos: Query<&Transform, With<SelfPlayer>>,
|
||||
self_pos: Query<&Transform, With<ActiveMover>>,
|
||||
joints: Query<&GlobalTransform>,
|
||||
footsteps: Option<Res<Footsteps>>,
|
||||
creatures: Option<Res<Creatures>>,
|
||||
|
||||
@@ -66,7 +66,7 @@ use benilla_ui::script::UiScript;
|
||||
|
||||
use crate::go_templates::GameObjectTemplates;
|
||||
use crate::names::NameCache;
|
||||
use crate::net::{Guid, GuidIndex, NetCommands, NetEntity, ObjectStore, SelfPlayer};
|
||||
use crate::net::{ActiveMover, Guid, GuidIndex, NetCommands, NetEntity, ObjectStore, SelfPlayer};
|
||||
use crate::ui_pass::{UiQuad, UiQuads, UvRect};
|
||||
|
||||
/// The landmark rank radius in yards (`0x8116cc` = 694.444, VERIFIED).
|
||||
@@ -154,7 +154,7 @@ pub(super) type TrackedCandidates<'w, 's> = Query<
|
||||
&'static GlobalTransform,
|
||||
Option<&'static ObjectStore>,
|
||||
),
|
||||
Without<SelfPlayer>,
|
||||
Without<ActiveMover>,
|
||||
>;
|
||||
|
||||
/// The shared frame geometry the blip emitters draw in: the widget centre/side, the active
|
||||
|
||||
@@ -182,11 +182,32 @@ pub(crate) struct UnitSpeeds(pub(crate) MoveSpeeds);
|
||||
#[derive(Component, Clone, Default)]
|
||||
pub(crate) struct ObjectStore(pub(crate) ObjectFields);
|
||||
|
||||
/// Marks our own player's streamed entity (guid == [`SelfGuid`]). The renderer skips it — the player
|
||||
/// controller owns our avatar — but the controller reads its [`Transform`] to take control on login.
|
||||
/// Marks our own player's streamed entity (guid == [`SelfGuid`]). This is **identity** — "my
|
||||
/// character", the thing whose bags, auras, quest log and paper doll are mine. It is deliberately
|
||||
/// *not* "the thing I steer": that is [`ActiveMover`], which normally sits on the same entity and
|
||||
/// leaves it while you drive something else.
|
||||
#[derive(Component)]
|
||||
pub(crate) struct SelfPlayer;
|
||||
|
||||
/// **The one unit this client simulates locally and streams to the server** — the reference's
|
||||
/// active-mover global (`ds:0xc4da98`/`0xc4da9c`, written only by `SetActiveMover 0x6006e0`), as a
|
||||
/// marker on the entity that global names.
|
||||
///
|
||||
/// Normally our own body, so it rides alongside [`SelfPlayer`]. While we hold somebody else's reins
|
||||
/// — mind-controlling a creature, an Eye of Kilrogg — it moves to *that* entity and our own body
|
||||
/// keeps none of it. The two are separate stores in the reference and separate markers here, for
|
||||
/// the reason decision 0092 separates the camera eye from the active-player character: possession
|
||||
/// moves what you *drive* without moving who you *are*.
|
||||
///
|
||||
/// Exactly one entity carries it, and possibly none — a claimed mover whose object has not streamed
|
||||
/// in yet is nobody, never a silent fallback to our own body. That distinction is load-bearing:
|
||||
/// outbound `MSG_MOVE_*` carry no guid, so driving our body under a claimed creature's mover writes
|
||||
/// our pose onto the creature. [`crate::player`] owns the marker's placement; everything that
|
||||
/// simulates, animates from input, or streams a body reads `With<ActiveMover>`, and everything that
|
||||
/// replays *server* motion reads `Without<ActiveMover>`.
|
||||
#[derive(Component)]
|
||||
pub(crate) struct ActiveMover;
|
||||
|
||||
// ── ECS state: resources ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// The inbound event channel — drained each frame by [`apply_net_updates`].
|
||||
|
||||
@@ -6,7 +6,7 @@ use benilla_assets::coords::bevy_to_wow;
|
||||
use benilla_protocol::{EntityKind, MonsterMoveFacing};
|
||||
use bevy::prelude::*;
|
||||
|
||||
use super::super::{GuidIndex, NetEntity, ObjectStore, SelfPlayer};
|
||||
use super::super::{ActiveMover, GuidIndex, NetEntity, ObjectStore};
|
||||
use super::{yaw_of, RemoteMotion, Spline};
|
||||
|
||||
/// Radians/second an idle creature turns to face its target — a smooth turn, not a snap. Tunable
|
||||
@@ -75,7 +75,7 @@ pub(in crate::net) fn resolve_facing(
|
||||
/// stationary meleeing mob squares up on its victim even though the server sends **no** facing for it
|
||||
/// (vmangos `SetInFront` is server-only; verified by a live sniff — no `MONSTER_MOVE` arrives when you
|
||||
/// attack a standing mob). A moving unit (a [`Spline`]) faces its travel direction instead; a remote
|
||||
/// player ([`RemoteMotion`]) and our own avatar ([`SelfPlayer`]) own their facing — all excluded. The
|
||||
/// player ([`RemoteMotion`]) and the body we drive ([`ActiveMover`]) own their facing — all excluded. The
|
||||
/// turn is capped per frame at [`FACE_TARGET_TURN_RATE`], toward the horizontal bearing to the target.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(in crate::net) fn face_target(
|
||||
@@ -84,7 +84,7 @@ pub(in crate::net) fn face_target(
|
||||
index: Res<GuidIndex>,
|
||||
candidates: Query<
|
||||
(Entity, &NetEntity, &ObjectStore),
|
||||
(Without<Spline>, Without<RemoteMotion>, Without<SelfPlayer>),
|
||||
(Without<Spline>, Without<RemoteMotion>, Without<ActiveMover>),
|
||||
>,
|
||||
mut transforms: Query<&mut Transform>,
|
||||
// A remote player's latch belongs to the facing interp in [`super::remote`] — the cleanup
|
||||
|
||||
@@ -10,7 +10,7 @@ use bevy::time::Real;
|
||||
use crate::creature_anim::move_flags;
|
||||
use crate::player::{GRAVITY, TERMINAL_VELOCITY};
|
||||
|
||||
use super::super::{SelfPlayer, UnitSpeeds};
|
||||
use super::super::{ActiveMover, UnitSpeeds};
|
||||
use super::relay::{PendingMove, RelayChain, RelayMove};
|
||||
use super::{yaw_of, Spline};
|
||||
|
||||
@@ -361,7 +361,7 @@ pub(in crate::net) fn drain_pending_moves(
|
||||
time: Res<Time<Real>>,
|
||||
mut commands: Commands,
|
||||
mut landings: MessageWriter<crate::creature_anim::HardLanding>,
|
||||
mut q: Query<(Entity, &mut RemoteMotion), (Without<Spline>, Without<SelfPlayer>)>,
|
||||
mut q: Query<(Entity, &mut RemoteMotion), (Without<Spline>, Without<ActiveMover>)>,
|
||||
) {
|
||||
let now_ms = time.elapsed_secs_f64() * 1000.0;
|
||||
for (e, mut rm) in &mut q {
|
||||
@@ -376,7 +376,7 @@ pub(in crate::net) fn drain_pending_moves(
|
||||
/// *smoothly* between the sparse relay packets instead of snapping at the ~2 Hz heartbeat rate. The
|
||||
/// horizontal velocity is derived from the live `moveFlags` in the mover's facing frame at its run /
|
||||
/// run-back / swim speed; the `TURN_*` flags rotate the facing at `turn_rate`. A creature [`Spline`]
|
||||
/// (server-authored path) and our own avatar ([`SelfPlayer`]) are excluded — they have their own
|
||||
/// (server-authored path) and the body we drive ([`ActiveMover`]) are excluded — they have their own
|
||||
/// motion source.
|
||||
///
|
||||
/// **The step is then resolved against the world** ([`crate::player::mover::grounded_step`], decision
|
||||
@@ -428,7 +428,7 @@ pub(in crate::net) fn extrapolate_remote_units(
|
||||
Has<super::FacingStep>,
|
||||
Has<crate::transport::TransportRider>,
|
||||
),
|
||||
(Without<Spline>, Without<SelfPlayer>),
|
||||
(Without<Spline>, Without<ActiveMover>),
|
||||
>,
|
||||
) {
|
||||
use crate::creature_anim::{ease_strafe_yaw, strafe_body_offset, wrap_pi};
|
||||
|
||||
@@ -22,7 +22,7 @@ use bevy::prelude::*;
|
||||
use bevy::window::{CursorOptions, PrimaryWindow};
|
||||
|
||||
use crate::creature_anim::{move_flags, wrap_pi, BodyTwist, MovementState};
|
||||
use crate::net::{ClientCommand, NetCommands, SelfPlayer, TeleportMessage, WorldportMessage};
|
||||
use crate::net::{ActiveMover, ClientCommand, NetCommands, TeleportMessage, WorldportMessage};
|
||||
use crate::ui_script::InspectMode;
|
||||
use crate::ui_script::PointerOverUi;
|
||||
use benilla_assets::AssetSet;
|
||||
@@ -36,6 +36,8 @@ mod world_focus;
|
||||
// own `pub(super)` fields instead of widening them for a module outside.
|
||||
mod camera_saved;
|
||||
mod drunk;
|
||||
// Which single unit the client embodies (decision 1277) — the `ActiveMover` marker's owner.
|
||||
mod embody;
|
||||
mod follow;
|
||||
|
||||
mod gait;
|
||||
@@ -248,14 +250,24 @@ impl Plugin for PlayerPlugin {
|
||||
Update,
|
||||
wire_in::release_on_session_end.in_set(WorldStage::Input),
|
||||
)
|
||||
// Mirror our body's collision height onto `Player` for the swim arm (decision 0645).
|
||||
// A *continuous* sync rather than a one-shot at take-control, for the reason the
|
||||
// take-control branch itself records: a cross-map worldport re-streams the `SelfPlayer`
|
||||
// entity, so anything latched on that edge is lost on transfer. Before `control`, which
|
||||
// is where the swim depth lines are evaluated.
|
||||
// Which body the client drives at all (decision 1277). Strictly before everything that
|
||||
// reads the marker — the controller, and the collision-height mirror below.
|
||||
.add_systems(
|
||||
Update,
|
||||
mirror_self_collision_height
|
||||
embody::maintain_active_mover
|
||||
.in_set(WorldStage::Input)
|
||||
.before(control)
|
||||
.before(mirror_mover_collision_height),
|
||||
)
|
||||
// Mirror the driven body's collision height onto `Player` for the swim arm (decision
|
||||
// 0645). A *continuous* sync rather than a one-shot at take-control, for the reason the
|
||||
// take-control branch itself records: a cross-map worldport re-streams the entity, so
|
||||
// anything latched on that edge is lost on transfer — and a possession swaps it for a
|
||||
// body of an entirely different size. Before `control`, which is where the swim depth
|
||||
// lines are evaluated.
|
||||
.add_systems(
|
||||
Update,
|
||||
mirror_mover_collision_height
|
||||
.in_set(WorldStage::Input)
|
||||
.before(control),
|
||||
)
|
||||
@@ -290,15 +302,16 @@ impl Plugin for PlayerPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirror the streamed `SelfPlayer`'s [`crate::entities::CollisionHeight`] onto [`Player`] — the
|
||||
/// avatar's swim depth lines are fractions of it (decision 0645), and the swim arm runs off the
|
||||
/// resource, not the entity. One entity, one copy: no work until our body streams in, and it
|
||||
/// re-syncs itself after a worldport re-streams that entity under a new one.
|
||||
fn mirror_self_collision_height(
|
||||
/// Mirror the driven body's [`crate::entities::CollisionHeight`] onto [`Player`] — its swim depth
|
||||
/// lines are fractions of it (decision 0645), and the swim arm runs off the resource, not the
|
||||
/// entity. One entity, one copy: no work until a body streams in, and it re-syncs itself after a
|
||||
/// worldport re-streams that entity under a new one — or after a possession puts a different-sized
|
||||
/// body in our hands.
|
||||
fn mirror_mover_collision_height(
|
||||
mut player: ResMut<Player>,
|
||||
self_player: Query<&crate::entities::CollisionHeight, With<SelfPlayer>>,
|
||||
mover: Query<&crate::entities::CollisionHeight, With<ActiveMover>>,
|
||||
) {
|
||||
if let Ok(&h) = self_player.single() {
|
||||
if let Ok(&h) = mover.single() {
|
||||
if player.collision_height != h {
|
||||
player.collision_height = h;
|
||||
}
|
||||
@@ -386,10 +399,13 @@ fn control(
|
||||
// Avian's kinematic move-and-slide: sweeps the capsule against the streamed colliders (decision 0009).
|
||||
collide: benilla_world::collision::WorldCollision,
|
||||
mut cameras: Query<(&mut Transform, &mut FlyCam, &Camera)>,
|
||||
// The streamed self entity: we read its server pose to take control, then drive its transform
|
||||
// (feet position + facing) and feed its movement to the animation selector via `MovementState`. Its
|
||||
// body model is attached by the entity renderer through the same path as any other player (0041).
|
||||
mut self_player: Query<
|
||||
// **The body in our hands** — normally our own streamed avatar, and a possessed creature while
|
||||
// we hold its reins (decision 1277). We read its server pose to take control, then drive its
|
||||
// transform (feet position + facing) and feed its movement to the animation selector via
|
||||
// `MovementState`. Its model is attached by the entity renderer through the same path as any
|
||||
// other unit (0041), which is exactly why a creature needs nothing special here: everything
|
||||
// below reads the body's own pivot, speeds, scale and descriptor off the entity.
|
||||
mut body: Query<
|
||||
(
|
||||
Entity,
|
||||
&mut Transform,
|
||||
@@ -404,7 +420,7 @@ fn control(
|
||||
// (the ref's three `GetWeapon(0/1/2)` calls before the state walk, below).
|
||||
Option<&crate::creature_anim::Wielded>,
|
||||
),
|
||||
(With<SelfPlayer>, Without<FlyCam>),
|
||||
(With<ActiveMover>, Without<FlyCam>),
|
||||
>,
|
||||
window: Single<(&mut Window, &mut CursorOptions), With<PrimaryWindow>>,
|
||||
// Clicks go out here — left for the target picker, right for the context action (attack) — and
|
||||
@@ -432,7 +448,7 @@ fn control(
|
||||
(&Transform, &crate::net::Guid),
|
||||
(
|
||||
With<crate::transport::Transport>,
|
||||
Without<SelfPlayer>,
|
||||
Without<ActiveMover>,
|
||||
Without<FlyCam>,
|
||||
),
|
||||
>,
|
||||
@@ -496,7 +512,7 @@ fn control(
|
||||
// movement flag and NOT an aura: the reference's `0x5145b0` computes `!STUNNED` straight off
|
||||
// `[[unit+0x110]+0xa0]` (`not eax; shr eax,0x12; and eax,1`) and `0x514755` consumes it to skip
|
||||
// the turn and pitch emitters outright.
|
||||
let stunned = self_player
|
||||
let stunned = body
|
||||
.single()
|
||||
.ok()
|
||||
.and_then(|(.., store, _, _, _, _)| {
|
||||
@@ -508,7 +524,7 @@ fn control(
|
||||
// fraction), and zero while stunned — the reference's wobble sits behind the same
|
||||
// input-allowed chain as the turn emitters (`0x60aa47` → `0x5145b0`).
|
||||
let drunk_wobble = {
|
||||
let f = self_player
|
||||
let f = body
|
||||
.single()
|
||||
.ok()
|
||||
.and_then(|(.., store, _, _, _, _)| store.and_then(|s| s.0.player_drunk_byte()))
|
||||
@@ -550,9 +566,13 @@ fn control(
|
||||
// zeroed, `0x514640` skips the whole tick at `51466c` — input is still *sampled*, the body turn
|
||||
// is skipped at `514474` — while the camera rotate at `514444` happens BEFORE the mover lookup
|
||||
// and so keeps working. A mind-controlled player can still look around; they just cannot turn
|
||||
// or move their body. Same restore, for the same reason, and it covers holding somebody else's
|
||||
// reins too: the turn belongs to whatever we are driving, which is not this body.
|
||||
if stunned || player.control_lost || player.foreign_mover.is_some() {
|
||||
// or move their body. Same restore, for the same reason — and `reseat` is the same condition
|
||||
// once more, the frames where the mover global would not resolve at all.
|
||||
//
|
||||
// Holding *somebody else's* reins is emphatically not in this set: the turn belongs to whatever
|
||||
// we are driving, and once the marker has caught up that is the creature, whose `Transform` is
|
||||
// the one this `face_yaw` writes.
|
||||
if stunned || player.control_lost || player.reseat {
|
||||
player.face_yaw = yaw_before_look;
|
||||
}
|
||||
let mouse_turned = player.face_yaw != yaw_before_look;
|
||||
@@ -600,26 +620,11 @@ fn control(
|
||||
&mut net.11,
|
||||
self_guid,
|
||||
transports,
|
||||
self_player
|
||||
.single()
|
||||
body.single()
|
||||
.ok()
|
||||
.map(|(_, t, ..)| (t.translation, server_ride::yaw_of(t.rotation))),
|
||||
);
|
||||
|
||||
// A mover the server took back owes it a parting pose (`CMSG_MOVE_NOT_ACTIVE_MOVER`), and this
|
||||
// is the first point in the frame that has one. Without it the server never clears its
|
||||
// confirmed-mover state and never re-broadcasts the stop, so observers keep the unit sliding on
|
||||
// its last relayed pose.
|
||||
if let Some(guid) = player.release_mover.take() {
|
||||
let _ = net.0 .0.send(crate::net::ClientCommand::NotActiveMover {
|
||||
guid,
|
||||
flags: 0,
|
||||
pos: benilla_assets::coords::bevy_to_wow(player.pos),
|
||||
orientation: player.face_yaw.rem_euclid(std::f32::consts::TAU),
|
||||
fall_time: 0,
|
||||
});
|
||||
}
|
||||
|
||||
let flat = |v: Vec3| Vec3::new(v.x, 0.0, v.z).normalize_or_zero();
|
||||
|
||||
// The platform carry (decision 0438 phase 2): while attached to a transport, recompose the
|
||||
@@ -684,13 +689,18 @@ fn control(
|
||||
// The park below flushes one `MSG_MOVE_STOP`, which matters — we may have been mid-run at
|
||||
// the moment control was taken, and the server would otherwise keep extrapolating that run
|
||||
// for every observer.
|
||||
// The same gate serves both ways of not driving our own body: the reins taken from us
|
||||
// (mind-controlled), or the reins of something else in our hands. The second is the
|
||||
// sharper one — outbound moves carry no guid, so with a foreign mover claimed, streaming
|
||||
// our body's pose would land on the possessed unit and teleport it onto us. Standing our
|
||||
// own character still is also what possession looks like from the outside.
|
||||
if player.control_lost || player.foreign_mover.is_some() {
|
||||
let pivot_h = self_player
|
||||
//
|
||||
// The same gate serves the other way of holding no reins: [`Player::reseat`], the window
|
||||
// between mover guids where what we intend to drive and what carries `ActiveMover` have
|
||||
// not yet met — the frame a grant lands, and every frame after it while the claimed unit
|
||||
// has not streamed in. Driving during that window writes one body's pose onto another,
|
||||
// because outbound moves carry no guid of their own. `apply_server_moves` above closes the
|
||||
// window the moment a pose is there to adopt, so this is normally a single frame.
|
||||
//
|
||||
// Note this branch is deliberately NOT "we are possessing": once the marker has caught up,
|
||||
// possession runs the *ordinary* controlled path below, on the creature (decision 1277).
|
||||
if player.control_lost || player.reseat {
|
||||
let pivot_h = body
|
||||
.single()
|
||||
.map(|(_, t, _, pivot, ..)| head_height(pivot, t.scale.x))
|
||||
.unwrap_or(CAM_PIVOT_FALLBACK);
|
||||
@@ -715,7 +725,7 @@ fn control(
|
||||
return;
|
||||
}
|
||||
if player.server_riding {
|
||||
let pivot_h = self_player
|
||||
let pivot_h = body
|
||||
.single()
|
||||
.map(|(_, t, _, pivot, ..)| head_height(pivot, t.scale.x))
|
||||
.unwrap_or(CAM_PIVOT_FALLBACK);
|
||||
@@ -848,6 +858,18 @@ fn control(
|
||||
// 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;
|
||||
// **The mover's own** six speeds, read once here for the whole frame — the turn below and
|
||||
// the run/backpedal selection further down. All six live on the driven unit's `CMovement`
|
||||
// and nothing in the reference's applied-input path ever reads *our* speeds when the mover
|
||||
// is a different object (VERIFIED, decision 1278), so a possessed creature moves and turns
|
||||
// at its own numbers for free: the component was on its entity all along.
|
||||
let mover_speeds = body.single().ok().and_then(|q| q.7).map(|s| s.0);
|
||||
// The 6th speed. Zero is the ctor state, not a rate — the client keeps no default of its
|
||||
// own, so a unit whose create block has not landed falls back rather than freezing solid.
|
||||
let turn_rate = mover_speeds
|
||||
.map(|s| s.turn_rate)
|
||||
.filter(|r| *r > 0.0)
|
||||
.unwrap_or(TURN_RATE);
|
||||
// This frame's keyboard-turn rotation — `seat_camera` carries the camera by it rigidly
|
||||
// (char and camera turn as one on the reference; director's call, closing 0050's open
|
||||
// "camera follow on turn" feel item).
|
||||
@@ -860,8 +882,10 @@ fn control(
|
||||
if turn_right {
|
||||
turn -= 1.0;
|
||||
}
|
||||
// 0.75× while also translating — the verified `flags & 0x200f` case.
|
||||
let rate = TURN_RATE * if translating { TURN_RATE_MOVING } else { 1.0 };
|
||||
// 0.75× while translating **or falling** — the verified `flags & 0x200f` case, whose
|
||||
// `0x2000` is FALLING (`0x7c5c73`). A jump mid-turn keeps the reduced rate.
|
||||
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;
|
||||
}
|
||||
@@ -915,7 +939,7 @@ fn control(
|
||||
// observer's. `stand_pending` is the local commit (the client's `SetStandState`
|
||||
// applies immediately and sends, one setter — `0x6127b0`), overlaid on the echoed
|
||||
// byte until it lands so the pose never waits on the round-trip.
|
||||
let stand_byte = self_player
|
||||
let stand_byte = body
|
||||
.single()
|
||||
.ok()
|
||||
.and_then(|(.., store, _, _, _, _)| store.map(|s| s.0.unit_stand_state()))
|
||||
@@ -957,7 +981,7 @@ fn control(
|
||||
// wow-re `sheath-policy.md` §4): entering any stand-state ∉ {0 STAND, 2 SIT_CHAIR}
|
||||
// force-stows drawn weapons, through the anim layer's one setter.
|
||||
if s != 0 && s != 2 {
|
||||
if let Ok((e, _, _, _, drv, _, _, _, _, _)) = self_player.single() {
|
||||
if let Ok((e, _, _, _, drv, _, _, _, _, _)) = body.single() {
|
||||
if drv.and_then(|d| d.sheath_state()).unwrap_or(0) != 0 {
|
||||
net.3.write(crate::creature_anim::SheathRequest {
|
||||
entity: e,
|
||||
@@ -978,8 +1002,7 @@ fn control(
|
||||
// `sheath-policy.md`). No body model yet (no driver) drops the toggle, the client's own
|
||||
// refusal.
|
||||
if binds.fired(crate::bindings::cmd::TOGGLE_SHEATH) {
|
||||
if let Ok((e, _, _, _, Some(drv), store, engaged, _, _, wielded)) = self_player.single()
|
||||
{
|
||||
if let Ok((e, _, _, _, Some(drv), store, engaged, _, _, wielded)) = body.single() {
|
||||
// The manual toggle's guard chain (decision 0080d) — the guards of the client's
|
||||
// 12-deep silent-refusal chain (`ToggleSheath` `0x5eb480`) whose states exist
|
||||
// today: dead · engaged in combat · not standing (`GetStandState() != 0` —
|
||||
@@ -1031,8 +1054,7 @@ fn control(
|
||||
// move us at the server's number). `$WOW_MOVE_SPEED` stays the absolute dev override
|
||||
// (backpedal keeps the vanilla 4.5/7.0 ratio under it); pre-create frames fall back the
|
||||
// same way.
|
||||
let server_speeds = self_player.single().ok().and_then(|q| q.7).map(|s| s.0);
|
||||
let (run_speed, run_back_speed) = match server_speeds {
|
||||
let (run_speed, run_back_speed) = match mover_speeds {
|
||||
Some(s) if !move_speed.env_override => (s.run, s.run_back),
|
||||
_ => (move_speed.value, move_speed.value * RUN_BACK_RATIO),
|
||||
};
|
||||
@@ -1111,7 +1133,7 @@ fn control(
|
||||
// (TU-F: Space is the Jump command; it is NOT a pitch or ascend input) — and never
|
||||
// reaches this walk-side gate.
|
||||
if want_jump && !moving && !swimming && player.airborne_since.is_none() {
|
||||
if let Ok((e, .., store, _, _, _, _)) = self_player.single() {
|
||||
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 {
|
||||
@@ -1196,7 +1218,7 @@ fn control(
|
||||
// forward or strafe-only → swim; the backward bit `0x2` → `min(swimBack, swim)` —
|
||||
// byte-identical in template to the run arm's `min(runBack, run)`. Vanilla defaults
|
||||
// 4.722/2.5 (vmangos `baseMoveSpeed`).
|
||||
let (swim_speed, swim_back_speed) = match server_speeds {
|
||||
let (swim_speed, swim_back_speed) = match mover_speeds {
|
||||
Some(s) if !move_speed.env_override => (s.swim, s.swim_back),
|
||||
_ => (swim::SWIM_SPEED, swim::SWIM_BACK_SPEED),
|
||||
};
|
||||
@@ -1531,6 +1553,7 @@ fn control(
|
||||
moving,
|
||||
airborne,
|
||||
turning || mouselook,
|
||||
turn_rate,
|
||||
);
|
||||
|
||||
// Drive the streamed self entity: its transform is the avatar's pose (feet position + body
|
||||
@@ -1542,7 +1565,7 @@ fn control(
|
||||
// `CAM_PIVOT_FLOOR`). Read the avatar's model-local pivot + its live scale here (where we hold the
|
||||
// self entity); until the body attaches (no `CameraPivot` yet) fall back to a human neck height.
|
||||
let mut cam_pivot_height = CAM_PIVOT_FALLBACK;
|
||||
if let Ok((entity, mut t, motion, pivot, .., twist, _)) = self_player.single_mut() {
|
||||
if let Ok((entity, mut t, motion, pivot, .., twist, _)) = body.single_mut() {
|
||||
t.translation = player.pos;
|
||||
// The swim body pitch (TU-A, `0x60a110`→`0x710620`): while swimming AND moving fwd/back
|
||||
// the model root renders `Rz(yaw)·Ry(−pitch)` — in Bevy axes, the yaw then a nose-up
|
||||
|
||||
@@ -12,7 +12,7 @@ use bevy::window::{CursorGrabMode, CursorOptions};
|
||||
|
||||
use avian3d::prelude::*;
|
||||
|
||||
use crate::net::SelfPlayer;
|
||||
use crate::net::ActiveMover;
|
||||
use benilla_assets::materials::WowModelMaterial;
|
||||
use benilla_world::interact::{WorldClick, WorldRightClick, WorldRightPress};
|
||||
use benilla_world::model_fade::{
|
||||
@@ -334,7 +334,7 @@ impl FlyCam {
|
||||
/// The per-model camera-pivot height in **model-local yards, pre-scale** — `attach17.z + 0.0972` (M2
|
||||
/// attachment id 17) for a character, else `0.9 × vertex-box Z-extent`; the reference's camera-target
|
||||
/// height (`0x50cbc0`, wow-re `follow-camera`). Stamped on every modeled unit at attach
|
||||
/// ([`crate::entities`]); `control` reads it off the [`SelfPlayer`], multiplies the live avatar scale,
|
||||
/// ([`crate::entities`]); `control` reads it off the [`ActiveMover`], multiplies that body's live scale,
|
||||
/// and floors at [`CAM_PIVOT_FLOOR`] to get the world pivot the third-person camera looks at (and the
|
||||
/// first-person eye). `0.0` for a bounds-less display (→ floor).
|
||||
#[derive(Component, Clone, Copy)]
|
||||
@@ -680,7 +680,7 @@ pub(super) fn seat_camera(
|
||||
#[allow(clippy::type_complexity, clippy::too_many_arguments)] // one Bevy system's full input set
|
||||
pub(crate) fn apply_self_model_fade(
|
||||
rig: Res<CameraControl>,
|
||||
self_player: Query<(Entity, Option<&crate::aura_visual::AuraNodes>), With<SelfPlayer>>,
|
||||
self_player: Query<(Entity, Option<&crate::aura_visual::AuraNodes>), With<ActiveMover>>,
|
||||
children_of: Query<&Children>,
|
||||
mut parts: Query<
|
||||
(
|
||||
@@ -1061,7 +1061,7 @@ mod tests {
|
||||
app.add_systems(Update, apply_self_model_fade);
|
||||
|
||||
// The avatar: root -> joint (the eye-glow bone). Its card follows the joint.
|
||||
let avatar = app.world_mut().spawn(SelfPlayer).id();
|
||||
let avatar = app.world_mut().spawn(ActiveMover).id();
|
||||
let joint = app.world_mut().spawn(Transform::default()).id();
|
||||
app.world_mut().entity_mut(avatar).add_child(joint);
|
||||
let eye_glow = app
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
//! **What the client embodies** — which single unit it simulates, animates from input, and streams.
|
||||
//!
|
||||
//! Decision 0092 gave this client two answers to "where am I?": the **camera eye** and the
|
||||
//! **active-player character**. Possession forces a third, and the reference has had it all along
|
||||
//! as its own global: the **active mover** (`ds:0xc4da98`, written only by `SetActiveMover
|
||||
//! 0x6006e0`), which the input applier `0x514640` resolves at the top of every tick and *skips the
|
||||
//! whole tick* when it does not resolve. The three are genuinely independent stores there — the
|
||||
//! camera never consults the mover to pick its anchor, and neither of them touches "the active
|
||||
//! player" (`ds:0xb41414`), which is invariant under both far sight and possession (VERIFIED,
|
||||
//! wow-re `object-layer/scratch/farsight-and-client-control.md` §9).
|
||||
//!
|
||||
//! So benilla splits the *marker*, not the identity. [`SelfPlayer`] keeps meaning **my character**
|
||||
//! — bags, auras, quest log, paper doll, the name over the head — and never moves.
|
||||
//! [`ActiveMover`] means **the body in my hands**, and moves. Of ~150 `SelfPlayer` query sites the
|
||||
//! large majority are the first kind and are untouched by possession; the ones that changed are
|
||||
//! exactly those that simulate, animate from input, render the body the camera is inside, or
|
||||
//! stream it.
|
||||
//!
|
||||
//! Three properties of the placement carry the weight:
|
||||
//!
|
||||
//! - **Forbidden is nobody too.** A body we are told we may not move — ours under a Mind Control,
|
||||
//! or a possessed creature the moment it is feared — leaves the marker unplaced, mirroring the
|
||||
//! reference's own zeroing of the mover global. That is what lets the *server* drive it: the
|
||||
//! ordinary relayed-motion path skips whatever carries this marker, so a body that keeps it while
|
||||
//! the controller refuses to drive it is driven by nobody at all, and stands frozen while the
|
||||
//! server runs it around in front of everybody else.
|
||||
//! - **Unresolvable is nobody, never a fallback.** A claimed grant whose object has not streamed in
|
||||
//! leaves the marker unplaced, because the alternative — quietly leaving it on our own body —
|
||||
//! drives our body under the creature's mover, and outbound `MSG_MOVE_*` carry no guid: the
|
||||
//! server writes our pose onto the creature. That is the sharpest trap in this family (decision
|
||||
//! 1269 §3), and here it is structurally impossible rather than guarded against.
|
||||
//! - **The reins change hands by GUID, not by entity.** A cross-map worldport despawns and
|
||||
//! re-streams our own body under a fresh entity while we never stop driving it, so keying the
|
||||
//! handover on the entity would re-seize (and re-settle) on every zone transfer.
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use super::follow::FollowState;
|
||||
use super::state::Player;
|
||||
use crate::creature_anim::MovementState;
|
||||
use crate::net::{ActiveMover, GuidIndex, RemoteMotion, SelfGuid, SelfPlayer};
|
||||
|
||||
/// Keep [`ActiveMover`] on the one body we drive: the possessed unit while we hold its reins, our
|
||||
/// own otherwise, and nobody at all while neither is streamed.
|
||||
///
|
||||
/// Runs before the controller, which reads the marker to decide what — if anything — it is driving.
|
||||
pub(super) fn maintain_active_mover(
|
||||
mut commands: Commands,
|
||||
mut player: ResMut<Player>,
|
||||
mut follow: ResMut<FollowState>,
|
||||
guids: (Res<SelfGuid>, Res<GuidIndex>),
|
||||
self_body: Query<Entity, With<SelfPlayer>>,
|
||||
tagged: Query<Entity, With<ActiveMover>>,
|
||||
mut driving: Local<Option<u64>>,
|
||||
) {
|
||||
let (self_guid, index) = (&guids.0, &guids.1);
|
||||
// Who we mean to drive, and which entity that is. Three answers, not two:
|
||||
//
|
||||
// - **Forbidden to move it** ([`Player::control_lost`]) — nobody. This is the reference's own
|
||||
// rule rather than an optimisation: `0x5fa600` *zeroes the mover global* for the named unit,
|
||||
// and the consequence is the whole point. A mind-controlled player's body stops being locally
|
||||
// simulated and falls back to the ordinary relayed-motion path, so it visibly walks where its
|
||||
// captor drives it instead of standing frozen while the server runs it around. The same for a
|
||||
// possessed creature that has just been feared: the server's flee path drives it, and we watch
|
||||
// through a camera that is still on it.
|
||||
// - **A claimed foreign mover** answers by itself: either its object is streamed and it is the
|
||||
// mover, or nothing is.
|
||||
// - Otherwise our own body.
|
||||
let want_guid = if player.control_lost {
|
||||
None
|
||||
} else {
|
||||
player.foreign_mover.or(self_guid.0)
|
||||
};
|
||||
let want = match (player.control_lost, player.foreign_mover) {
|
||||
(true, _) => None,
|
||||
(false, Some(guid)) => index.0.get(&guid).copied(),
|
||||
(false, None) => self_body.iter().next(),
|
||||
};
|
||||
|
||||
if *driving != want_guid {
|
||||
*driving = want_guid;
|
||||
if want_guid.is_some() {
|
||||
// Whatever pose and momentum `Player` holds describe the body we just let go of, so the
|
||||
// controller has to adopt the new one's before it drives anything. It also drives
|
||||
// nothing at all until it has: see [`Player::reseat`].
|
||||
player.reseat = true;
|
||||
// The reference tears the same thing down on the outgoing mover — `SetActiveMover
|
||||
// 0x6006e0` calls `0x6103a0` → `0x60fb60(0, 1)`, cancelling click-to-move and follow.
|
||||
// A follow that survived would steer the creature toward whoever our *character* was
|
||||
// walking behind.
|
||||
follow.stop();
|
||||
}
|
||||
}
|
||||
|
||||
let held = tagged.iter().next();
|
||||
if held == want {
|
||||
return;
|
||||
}
|
||||
for e in &tagged {
|
||||
commands.entity(e).remove::<ActiveMover>();
|
||||
// Hand the unit back to the ordinary remote path. [`MovementState`] is `unify`'s
|
||||
// *top-precedence* leg, so one left behind would freeze the creature's animation on the
|
||||
// last view we drove it with and shadow the relayed `MSG_MOVE_*` stream forever. Our own
|
||||
// body keeps its own — `net::apply::tag_self_player` owns that one and never re-inserts it.
|
||||
if !self_body.contains(e) {
|
||||
commands.entity(e).remove::<MovementState>();
|
||||
}
|
||||
}
|
||||
if let Some(e) = want {
|
||||
// Drop whatever server-replay state the unit carried at the instant we took it. While we
|
||||
// drive it the server sends us no relays for it (vmangos excludes the mover's own session
|
||||
// from the broadcast), so nothing accumulates — but a move already queued for a future
|
||||
// apply would otherwise fire after we let go and yank the unit back to a pose it left.
|
||||
commands
|
||||
.entity(e)
|
||||
.insert(ActiveMover)
|
||||
.remove::<RemoteMotion>();
|
||||
// A creature carries no controller-fed movement view — only our own avatar is given one,
|
||||
// beside its `SelfPlayer` tag. Without it the possessed unit would walk with its idle
|
||||
// animation playing: `control` writes this component, and `unify` reads it first.
|
||||
if !self_body.contains(e) {
|
||||
commands.entity(e).insert(MovementState::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::net::RemoteMotion;
|
||||
|
||||
fn harness() -> (App, Entity) {
|
||||
let mut app = App::new();
|
||||
app.init_resource::<Player>()
|
||||
.init_resource::<FollowState>()
|
||||
.init_resource::<SelfGuid>()
|
||||
.init_resource::<GuidIndex>()
|
||||
.add_systems(Update, maintain_active_mover);
|
||||
let me = app.world_mut().spawn(SelfPlayer).id();
|
||||
app.world_mut().resource_mut::<SelfGuid>().0 = Some(0xAA);
|
||||
app.world_mut()
|
||||
.resource_mut::<GuidIndex>()
|
||||
.0
|
||||
.insert(0xAA, me);
|
||||
(app, me)
|
||||
}
|
||||
|
||||
fn mover(app: &mut App) -> Option<Entity> {
|
||||
let mut q = app
|
||||
.world_mut()
|
||||
.query_filtered::<Entity, With<ActiveMover>>();
|
||||
q.iter(app.world()).next()
|
||||
}
|
||||
|
||||
/// A remote unit's replay state, minimally populated — only its presence is under test.
|
||||
fn replaying() -> RemoteMotion {
|
||||
RemoteMotion {
|
||||
wow_pos: [0.0; 3],
|
||||
orientation: 0.0,
|
||||
flags: 0,
|
||||
pitch: 0.0,
|
||||
speed: 0.0,
|
||||
vertical_velocity: 0.0,
|
||||
jump_xy_vel: [0.0; 2],
|
||||
fall_start_z: None,
|
||||
pending: std::collections::VecDeque::new(),
|
||||
relay: Default::default(),
|
||||
last_apply_ms: 0.0,
|
||||
last_apply_pos: [0.0; 3],
|
||||
}
|
||||
}
|
||||
|
||||
/// The whole handover, and the two states a casual version gets wrong: a claim we cannot yet
|
||||
/// resolve must leave the marker on **nobody**, and coming home must hand the creature back to
|
||||
/// the remote path intact.
|
||||
#[test]
|
||||
fn a_claim_we_cannot_resolve_yet_moves_the_marker_to_nobody_not_to_our_own_body() {
|
||||
let (mut app, me) = harness();
|
||||
app.update();
|
||||
assert_eq!(mover(&mut app), Some(me), "no claim → our own body");
|
||||
|
||||
// Claimed, but the creature has not streamed in.
|
||||
app.world_mut().resource_mut::<Player>().foreign_mover = Some(0xBB);
|
||||
app.update();
|
||||
assert_eq!(
|
||||
mover(&mut app),
|
||||
None,
|
||||
"an unresolvable claim is NOBODY — leaving it on our body drives us under the \
|
||||
creature's mover, and outbound moves carry no guid of their own"
|
||||
);
|
||||
assert!(
|
||||
app.world().resource::<Player>().reseat,
|
||||
"and the controller is told to drive nothing until it has a pose to adopt"
|
||||
);
|
||||
|
||||
// It streams in.
|
||||
let boar = app.world_mut().spawn(replaying()).id();
|
||||
app.world_mut()
|
||||
.resource_mut::<GuidIndex>()
|
||||
.0
|
||||
.insert(0xBB, boar);
|
||||
app.update();
|
||||
assert_eq!(mover(&mut app), Some(boar));
|
||||
assert!(
|
||||
app.world().entity(boar).contains::<MovementState>(),
|
||||
"a creature carries no controller-fed movement view of its own — without one it walks \
|
||||
with its idle animation playing"
|
||||
);
|
||||
assert!(
|
||||
!app.world().entity(boar).contains::<RemoteMotion>(),
|
||||
"and its queued server replay is dropped, or it snaps back the moment we let go"
|
||||
);
|
||||
|
||||
// Released.
|
||||
app.world_mut().resource_mut::<Player>().foreign_mover = None;
|
||||
app.update();
|
||||
assert_eq!(mover(&mut app), Some(me), "the reins come home");
|
||||
assert!(
|
||||
!app.world().entity(boar).contains::<MovementState>(),
|
||||
"`unify` reads this leg FIRST, so one left behind freezes the creature's animation on \
|
||||
the last view we drove it with, forever"
|
||||
);
|
||||
}
|
||||
|
||||
/// The one component this system must never take away. `tag_self_player` inserts our body's
|
||||
/// [`MovementState`] beside the `SelfPlayer` tag and never inserts it again, so stripping it
|
||||
/// here — as the creature leg does — would leave our own avatar animation-dead for the rest of
|
||||
/// the session, and only after a possession.
|
||||
#[test]
|
||||
fn our_own_body_never_loses_its_movement_view_to_a_possession() {
|
||||
let (mut app, me) = harness();
|
||||
app.world_mut()
|
||||
.entity_mut(me)
|
||||
.insert(MovementState::default());
|
||||
app.update();
|
||||
|
||||
app.world_mut().resource_mut::<Player>().foreign_mover = Some(0xBB);
|
||||
let boar = app.world_mut().spawn(replaying()).id();
|
||||
app.world_mut()
|
||||
.resource_mut::<GuidIndex>()
|
||||
.0
|
||||
.insert(0xBB, boar);
|
||||
app.update();
|
||||
app.world_mut().resource_mut::<Player>().foreign_mover = None;
|
||||
app.update();
|
||||
|
||||
assert!(
|
||||
app.world().entity(me).contains::<MovementState>(),
|
||||
"our own body's movement view is `tag_self_player`'s to own, never this system's"
|
||||
);
|
||||
}
|
||||
|
||||
/// A cross-map worldport despawns and re-streams our own body under a fresh entity while we
|
||||
/// never stop driving it. Keying the handover on the entity would re-seize — and re-settle —
|
||||
/// on every zone transfer.
|
||||
#[test]
|
||||
fn re_streaming_our_own_body_moves_the_marker_without_calling_it_a_handover() {
|
||||
let (mut app, me) = harness();
|
||||
app.update();
|
||||
app.world_mut().resource_mut::<Player>().reseat = false;
|
||||
|
||||
app.world_mut().entity_mut(me).despawn();
|
||||
let reborn = app.world_mut().spawn(SelfPlayer).id();
|
||||
app.world_mut()
|
||||
.resource_mut::<GuidIndex>()
|
||||
.0
|
||||
.insert(0xAA, reborn);
|
||||
app.update();
|
||||
|
||||
assert_eq!(
|
||||
mover(&mut app),
|
||||
Some(reborn),
|
||||
"the marker follows the entity"
|
||||
);
|
||||
assert!(
|
||||
!app.world().resource::<Player>().reseat,
|
||||
"but the mover GUID never changed, so this is not a handover: re-seizing here would \
|
||||
discard the worldport's own snap and re-run the settle"
|
||||
);
|
||||
}
|
||||
/// Being forbidden to move a body hands it back to the *server's* motion, and that is visible:
|
||||
/// a mind-controlled player must be seen walking where their captor drives them. The marker
|
||||
/// staying on our own body is what would freeze it — `control` refuses to drive it, and the
|
||||
/// remote-replay path skips whatever carries the marker, so it would be driven by nobody at all.
|
||||
#[test]
|
||||
fn a_body_we_may_not_move_is_driven_by_nobody_so_the_server_can_drive_it() {
|
||||
let (mut app, me) = harness();
|
||||
app.update();
|
||||
assert_eq!(mover(&mut app), Some(me));
|
||||
|
||||
// Mind-controlled: the server named us with allowMove = 0.
|
||||
app.world_mut().resource_mut::<Player>().control_lost = true;
|
||||
app.update();
|
||||
assert_eq!(
|
||||
mover(&mut app),
|
||||
None,
|
||||
"the reins are nobody's — our body rejoins the relayed-motion path and walks where it \
|
||||
is driven, instead of standing frozen while the server runs it around"
|
||||
);
|
||||
|
||||
// And a possessed creature we have just been forbidden to move behaves the same way.
|
||||
app.world_mut().resource_mut::<Player>().foreign_mover = Some(0xBB);
|
||||
let boar = app.world_mut().spawn(replaying()).id();
|
||||
app.world_mut()
|
||||
.resource_mut::<GuidIndex>()
|
||||
.0
|
||||
.insert(0xBB, boar);
|
||||
app.update();
|
||||
assert_eq!(mover(&mut app), None, "held, but not ours to move");
|
||||
|
||||
// Control back: the marker returns, and the body under us is re-adopted — it moved while
|
||||
// the server had it.
|
||||
app.world_mut().resource_mut::<Player>().reseat = false;
|
||||
app.world_mut().resource_mut::<Player>().control_lost = false;
|
||||
app.update();
|
||||
assert_eq!(mover(&mut app), Some(boar));
|
||||
assert!(
|
||||
app.world().resource::<Player>().reseat,
|
||||
"the creature ran under the server's fear path; carrying our stale pose onto it would \
|
||||
snap it back to wherever we last drove it"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
use crate::creature_anim::{ease_strafe_yaw, move_flags, strafe_body_offset, wrap_pi};
|
||||
|
||||
use super::{Player, STATIONARY_CHASE_RATE, TURN_RATE};
|
||||
use super::{Player, STATIONARY_CHASE_RATE};
|
||||
|
||||
/// Advance `model_yaw` (the rendered body heading) one frame and return the **animation's** view of
|
||||
/// the move flags (the wire keeps the real key flags — observers integrate the facing from them):
|
||||
@@ -41,6 +41,9 @@ pub(super) fn drive_body_heading(
|
||||
moving: bool,
|
||||
airborne: bool,
|
||||
steering: bool,
|
||||
// The mover's own turn rate (rad/s) — the release sweep is a multiple of it, so a possessed
|
||||
// creature's body settles onto its aim at the creature's pace (decision 1278).
|
||||
turn_rate: f32,
|
||||
) -> u32 {
|
||||
let strafe_offset = if swimming {
|
||||
0.0
|
||||
@@ -58,7 +61,7 @@ pub(super) fn drive_body_heading(
|
||||
let delta = wrap_pi(player.face_yaw - player.model_yaw);
|
||||
let mut step = (delta.abs() - std::f32::consts::FRAC_PI_2).max(0.0); // the ceiling
|
||||
if !steering {
|
||||
step += dt * TURN_RATE * STATIONARY_CHASE_RATE; // the release sweep
|
||||
step += dt * turn_rate * STATIONARY_CHASE_RATE; // the release sweep
|
||||
}
|
||||
body_turn_step = step.min(delta.abs()).copysign(delta);
|
||||
player.model_yaw = wrap_pi(player.model_yaw + body_turn_step);
|
||||
@@ -75,3 +78,53 @@ pub(super) fn drive_body_heading(
|
||||
}
|
||||
anim_flags
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The release sweep is a multiple of the **mover's** turn rate, not of a constant. It is the
|
||||
/// one place the rate reaches the *rendered* body rather than the aim, so a possessed creature
|
||||
/// with a slow turn rate would otherwise snap its body around at a human's pace while its
|
||||
/// facing crawled — the head and the body disagreeing about what they are attached to.
|
||||
#[test]
|
||||
fn the_standing_body_sweeps_back_at_the_movers_own_rate() {
|
||||
let sweep = |turn_rate: f32| {
|
||||
let mut player = Player {
|
||||
face_yaw: 1.0,
|
||||
model_yaw: 0.0,
|
||||
..Default::default()
|
||||
};
|
||||
drive_body_heading(&mut player, 0, 0.01, false, false, false, false, turn_rate);
|
||||
player.model_yaw
|
||||
};
|
||||
// Below the 90° ceiling, the whole step is the sweep — so it scales linearly with the rate.
|
||||
let slow = sweep(std::f32::consts::PI / 4.0);
|
||||
let fast = sweep(std::f32::consts::PI);
|
||||
assert!(slow > 0.0 && fast > slow);
|
||||
assert!(
|
||||
(fast / slow - 4.0).abs() < 1e-3,
|
||||
"a 4× turn rate sweeps 4× as far in a frame: {slow} vs {fast}"
|
||||
);
|
||||
// Steering freezes the chase whatever the rate — the byte rule, not a slow sweep.
|
||||
let mut player = Player {
|
||||
face_yaw: 1.0,
|
||||
model_yaw: 0.0,
|
||||
..Default::default()
|
||||
};
|
||||
drive_body_heading(
|
||||
&mut player,
|
||||
0,
|
||||
0.01,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
std::f32::consts::PI,
|
||||
);
|
||||
assert_eq!(
|
||||
player.model_yaw, 0.0,
|
||||
"under the 90° ceiling a steering frame moves the body not at all"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ use benilla_assets::coords::bevy_to_wow;
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::creature_anim::{move_flags, BodyTwist, MovementState};
|
||||
use crate::net::{ClientCommand, NetCommands, SelfPlayer, Spline};
|
||||
use crate::net::{ActiveMover, ClientCommand, NetCommands, Spline};
|
||||
|
||||
use super::Player;
|
||||
|
||||
@@ -54,7 +54,7 @@ pub(super) fn drive_self_ride(
|
||||
Option<&mut MovementState>,
|
||||
Option<&mut BodyTwist>,
|
||||
),
|
||||
With<SelfPlayer>,
|
||||
With<ActiveMover>,
|
||||
>,
|
||||
) {
|
||||
// Only while we hold control (post-login). A free-fly detach (`F`) abandons any ride rather than
|
||||
@@ -186,7 +186,7 @@ mod tests {
|
||||
grounded: true,
|
||||
},
|
||||
twist,
|
||||
SelfPlayer,
|
||||
ActiveMover,
|
||||
))
|
||||
.id();
|
||||
(app, entity, rx)
|
||||
|
||||
@@ -19,10 +19,20 @@ use bevy::prelude::*;
|
||||
/// ~36% shorter — no separate constant.
|
||||
pub(super) const RUN_BACK_RATIO: f32 = 4.5 / 7.0;
|
||||
|
||||
/// Character turn rate (rad/s) — how fast A/D rotate the avatar's facing when not mouse-looking (the
|
||||
/// vanilla turn-vs-strafe model, VERIFIED wow-5875-re `0x7c4f30` heading integrate). It is the unit's
|
||||
/// 6th movement speed (`CMovement+0x9c`, the server-seeded `TURN_RATE`; vanilla default ≈π rad/s),
|
||||
/// reduced to 0.75× while also translating (the `flags & 0x200f` case). Decision 0050.
|
||||
/// Turn rate (rad/s) **fallback** — how fast A/D rotate the facing when not mouse-looking, used
|
||||
/// only until the mover's own sixth movement speed arrives.
|
||||
///
|
||||
/// The live rate is per-unit and server-authoritative: `CMovement+0x9c`, last of six consecutive
|
||||
/// speed floats on the **mover's own** `CMovement`, set by `0x7c6ff0` (which acks with
|
||||
/// `CMSG_FORCE_TURN_RATE_CHANGE_ACK`, `0x2df`) and read for held turns by `GetYawRate 0x7c5c50`.
|
||||
/// The base ctor `0x7c4850` *zeroes* all six, so the client keeps no default of its own — π is the
|
||||
/// vanilla server's value, and this constant stands in only for frames before a create block lands.
|
||||
/// VERIFIED wow-5875-re (`0x7c4f30` heading integrate; the speed block, decision 1278).
|
||||
///
|
||||
/// Because it lives on the mover, a possessed creature turns at **its** rate, not ours. Reduced to
|
||||
/// [`TURN_RATE_MOVING`] while translating **or falling** (`flags & 0x200f`), and the same cell is
|
||||
/// the held-*pitch* rate — that integrator's keys are default-unbound, so we never bind it.
|
||||
/// Decision 0050.
|
||||
pub(super) const TURN_RATE: f32 = std::f32::consts::PI;
|
||||
|
||||
/// The mouselook swim-pitch clamp (radians) — **VERIFIED** ±89.0° = 1.5533431 (`0x8089d8` =
|
||||
@@ -468,9 +478,19 @@ pub(crate) struct Player {
|
||||
pub(super) follow_forward: bool,
|
||||
/// Free-fly (`F`): the camera moves on its own and the avatar/server position is frozen.
|
||||
pub(crate) detached: bool,
|
||||
/// **The server has taken the reins off us** — `SMSG_CLIENT_CONTROL_UPDATE` naming our own guid
|
||||
/// with `allowMove = 0`. Set while somebody is mind-controlling us; cleared when control comes
|
||||
/// back (B211).
|
||||
/// **The body in our hands may not be moved** — `SMSG_CLIENT_CONTROL_UPDATE` with
|
||||
/// `allowMove = 0` about whatever we are currently driving.
|
||||
///
|
||||
/// Two cases, one meaning (decision 1279). The packet names **us** while somebody is
|
||||
/// mind-controlling us (B211); it names **the creature we are possessing** when that creature
|
||||
/// becomes feared or confused, which vmangos sends from the fear/flee movement generators
|
||||
/// without ending the possession at all. Both say the same thing — the reins are still where
|
||||
/// they were, and nothing may move — and the reference collapses them the same way, by zeroing
|
||||
/// the mover global for whichever unit the packet names when it is the one already being
|
||||
/// driven.
|
||||
///
|
||||
/// So this is **not** "someone is controlling me"; reading it that way is what made the second
|
||||
/// case clear [`Player::foreign_mover`] and walk our own body around under a creature's mover.
|
||||
///
|
||||
/// This is the *whole* of what stops a mind-controlled player walking away, and that is a
|
||||
/// verified property of the server rather than an assumption: vmangos never roots the victim,
|
||||
@@ -483,11 +503,6 @@ pub(crate) struct Player {
|
||||
/// (decision 0866), each with its own SMSG/ack pair and its own movement-flag bit; this owes no
|
||||
/// ack, carries no flag, and suppresses turning too — which root explicitly does not.
|
||||
pub(crate) control_lost: bool,
|
||||
/// A mover the server just took back from us, still owed its `CMSG_MOVE_NOT_ACTIVE_MOVER`.
|
||||
///
|
||||
/// Parked here rather than sent at the wire drain because the release carries a *pose*, and the
|
||||
/// controller is the only thing that owns one. Cleared the frame it goes out.
|
||||
pub(crate) release_mover: Option<u64>,
|
||||
/// **We hold somebody else's reins** — a unit the server handed us and we claimed as our mover
|
||||
/// (mind-controlling a creature, Eye of Kilrogg). `None` whenever the mover is our own body.
|
||||
///
|
||||
@@ -497,10 +512,30 @@ pub(crate) struct Player {
|
||||
/// mover would teleport that creature onto us. Suppressing our body is also simply what
|
||||
/// possession looks like: your own character stands still while you drive the other thing.
|
||||
///
|
||||
/// Driving the held unit is the named residual (decision 1269): its capsule, speeds and
|
||||
/// take-control edge are all still the avatar's alone, so for now the possessed unit stays put
|
||||
/// rather than moving wrongly.
|
||||
/// While this holds, [`crate::net::ActiveMover`] sits on that unit's entity — or on nothing at
|
||||
/// all until it streams — and the controller drives *it*. Our own body then simulates nothing,
|
||||
/// animates from nothing, and sends nothing, which is both the fix and what possession looks
|
||||
/// like from the outside (decision 1277).
|
||||
pub(crate) foreign_mover: Option<u64>,
|
||||
/// **The reins are between hands** — the body we drive has changed, and we drive *nothing*
|
||||
/// until we have adopted its pose.
|
||||
///
|
||||
/// Set when the mover guid changes (the control handshake in [`super::wire_in`], and
|
||||
/// [`super::embody::maintain_active_mover`] when the marker follows it); cleared at the same
|
||||
/// take-control edge that seizes our own body on login, once a streamed pose is actually there
|
||||
/// to seize. Both halves matter:
|
||||
///
|
||||
/// - **Adopt before driving** — the resource's `pos`/facing still describe the body we were
|
||||
/// driving a moment ago, so the first streamed frame would otherwise report the creature
|
||||
/// standing wherever we left our character.
|
||||
/// - **Drive nothing while pending** — the handshake runs *inside* the controller, a frame
|
||||
/// ahead of the marker that follows it, and the claimed unit may not have streamed in at all.
|
||||
/// Either way there is a window where what we intend to drive and what carries the marker
|
||||
/// disagree, and driving during it means writing one body's pose onto another.
|
||||
///
|
||||
/// A flag rather than a direct write because the pose lives on the entity and the seize needs
|
||||
/// the camera too — both of which the controller already holds and the marker's owner does not.
|
||||
pub(crate) reseat: bool,
|
||||
/// Feet position in **Bevy** coords (converted to raw WoW only when sending to the server).
|
||||
pub(crate) pos: Vec3,
|
||||
/// Vertical velocity (yd/s, Bevy +Y up) for gravity/jump/fall. Integrated each frame; zeroed while
|
||||
|
||||
@@ -21,8 +21,8 @@ use benilla_assets::coords::{bevy_to_wow, wow_to_bevy};
|
||||
|
||||
use crate::creature_anim::{move_flags, wrap_pi};
|
||||
use crate::net::{
|
||||
ClientCommand, ClientControlMessage, Guid, MoveKind, MoveModeMessage, NetCommands,
|
||||
SelfMoveMessage, SelfPlayer, SpeedChangeMessage, TeleportMessage, WorldportMessage,
|
||||
ActiveMover, ClientCommand, ClientControlMessage, Guid, MoveKind, MoveModeMessage, NetCommands,
|
||||
SelfMoveMessage, SpeedChangeMessage, TeleportMessage, WorldportMessage,
|
||||
};
|
||||
use crate::transport::Transport;
|
||||
use benilla_world::world_map::CurrentMap;
|
||||
@@ -49,6 +49,32 @@ enum ControlVerdict {
|
||||
Released(u64),
|
||||
}
|
||||
|
||||
/// Give our own body back to the server: the stop, then the parting pose.
|
||||
///
|
||||
/// `CMSG_MOVE_NOT_ACTIVE_MOVER` (`0x2D1`) is **only ever about our own character**. The reference
|
||||
/// sends it from `SetActiveMover 0x6006e0` for the outgoing mover *when that mover was the local
|
||||
/// player* (`0x5fa6d0`), and vmangos agrees from the other side: `HandleMoveNotActiveMover` rejects
|
||||
/// any guid that is not the session's currently-confirmed mover, and rejects again if the guid names
|
||||
/// something other than the player while the player's mover still is it. There is no such packet for
|
||||
/// a creature we are handing back.
|
||||
///
|
||||
/// Skipped while a server-authored spline owns the body: the reference returns early on that edge
|
||||
/// (`0x619d50`), and vmangos would discard it anyway (`HasPendingSplineDone`).
|
||||
fn yield_own_body(net_cmds: &NetCommands, player: &mut Player, self_guid: Option<u64>) {
|
||||
movement_net::park_mover(&net_cmds.0, player);
|
||||
let Some(me) = self_guid else { return };
|
||||
if player.server_riding {
|
||||
return;
|
||||
}
|
||||
let _ = net_cmds.0.send(ClientCommand::NotActiveMover {
|
||||
guid: me,
|
||||
flags: 0,
|
||||
pos: bevy_to_wow(player.pos),
|
||||
orientation: player.face_yaw.rem_euclid(std::f32::consts::TAU),
|
||||
fall_time: 0,
|
||||
});
|
||||
}
|
||||
|
||||
/// Classify a control update. `self_guid` is `None` only before login has named us, where nothing
|
||||
/// can be "about me" yet.
|
||||
fn control_verdict(mover: u64, allow_move: bool, self_guid: Option<u64>) -> ControlVerdict {
|
||||
@@ -83,7 +109,7 @@ pub(super) fn apply_server_moves(
|
||||
self_guid: Option<u64>,
|
||||
transports: &Query<
|
||||
(&Transform, &Guid),
|
||||
(With<Transport>, Without<SelfPlayer>, Without<FlyCam>),
|
||||
(With<Transport>, Without<ActiveMover>, Without<FlyCam>),
|
||||
>,
|
||||
self_pose: Option<(Vec3, f32)>,
|
||||
) -> Vec<SpeedChangeMessage> {
|
||||
@@ -97,11 +123,27 @@ pub(super) fn apply_server_moves(
|
||||
// frozen server-side while our client cheerfully walks it around locally.
|
||||
for c in control_msgs.read() {
|
||||
match control_verdict(c.mover, c.allow_move, self_guid) {
|
||||
ControlVerdict::Revoked => player.control_lost = true,
|
||||
// Somebody is driving our body. Yielding it is not just bookkeeping: the parting
|
||||
// `CMSG_MOVE_NOT_ACTIVE_MOVER` clears the server's `m_clientMoverGuid`, after which
|
||||
// `GetConfirmedMover` answers null and vmangos **discards every movement packet we
|
||||
// send**. That is a second, server-side belt on the immobility our own gate provides.
|
||||
ControlVerdict::Revoked => {
|
||||
player.control_lost = true;
|
||||
yield_own_body(net_cmds, player, self_guid);
|
||||
}
|
||||
ControlVerdict::Restored => {
|
||||
player.control_lost = false;
|
||||
player.foreign_mover = None;
|
||||
player.reseat = true;
|
||||
// Re-claim ourselves as the mover — the same claim login makes.
|
||||
//
|
||||
// And send **nothing else**. A parting stop for the creature we were driving looks
|
||||
// right and is a teleport: by the time this packet is written the server has
|
||||
// already pointed `m_mover` back at us while `m_clientMoverGuid` still names the
|
||||
// creature, so `GetConfirmedMover` falls through its mismatch branch to *us* — and
|
||||
// a stop carrying the creature's pose would relocate our own character to wherever
|
||||
// the creature was standing. The creature's own stop is the server's to send; it
|
||||
// owns that unit again.
|
||||
let _ = net_cmds
|
||||
.0
|
||||
.send(ClientCommand::SetActiveMover { guid: c.mover });
|
||||
@@ -110,16 +152,35 @@ pub(super) fn apply_server_moves(
|
||||
// claim is what stops the controller streaming OUR body's pose under their guid —
|
||||
// outbound moves carry none of their own, so that would teleport them onto us.
|
||||
ControlVerdict::Granted(guid) => {
|
||||
// Hand our own body over FIRST, while it is still the mover the server attributes
|
||||
// to. One command channel, so this is genuinely ordered ahead of the claim — and
|
||||
// it has to be, in both directions: vmangos rejects a
|
||||
// `CMSG_MOVE_NOT_ACTIVE_MOVER` whose guid is not the mover it currently has
|
||||
// confirmed, and a `MSG_MOVE_STOP` sent after the claim would carry our pose under
|
||||
// the creature's guid.
|
||||
yield_own_body(net_cmds, player, self_guid);
|
||||
player.control_lost = false;
|
||||
player.foreign_mover = Some(guid);
|
||||
player.reseat = true;
|
||||
let _ = net_cmds.0.send(ClientCommand::SetActiveMover { guid });
|
||||
}
|
||||
// The matching `CMSG_MOVE_NOT_ACTIVE_MOVER` rides the controller's own park, which
|
||||
// owns the pose that has to go in it.
|
||||
// "That unit may not move" — about a unit that is not us. Genuinely ambiguous on the
|
||||
// wire, because vmangos sends it from two places: the unpossess path (right after the
|
||||
// `Restored` that already took us home) and, while a possession is still very much in
|
||||
// force, whenever the possessed unit becomes feared or confused.
|
||||
//
|
||||
// The reference resolves both with one rule, and so do we: *is this the unit I am
|
||||
// driving?* If it is, we are still holding it and merely forbidden to move it — the
|
||||
// mover global goes to nobody, exactly as `0x5fa600` zeroes it when the named unit is
|
||||
// the current one. If it is not, `Restored` already ran and there is nothing to do.
|
||||
//
|
||||
// Clearing `foreign_mover` here — the obvious reading — would send us back to driving
|
||||
// our own body while the server still has `m_mover` pointed at the creature, so every
|
||||
// step we took would be applied to the creature instead.
|
||||
ControlVerdict::Released(guid) => {
|
||||
if player.foreign_mover == Some(guid) {
|
||||
player.foreign_mover = None;
|
||||
player.control_lost = true;
|
||||
}
|
||||
player.release_mover = Some(guid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -287,22 +348,54 @@ pub(super) fn apply_server_moves(
|
||||
);
|
||||
}
|
||||
|
||||
// Take control once the server first reports our position (the streamed `SelfPlayer` entity,
|
||||
// whose transform is already in Bevy space). From here the controller drives that entity
|
||||
// directly; the entity renderer attaches its body model (0041) the same way it does for any
|
||||
// other player.
|
||||
if !player.active {
|
||||
// Take control once the server first reports our position (the streamed mover entity, whose
|
||||
// transform is already in Bevy space). From here the controller drives that entity directly;
|
||||
// the entity renderer attaches its body model (0041) the same way it does for any other player.
|
||||
//
|
||||
// The same edge serves a *mover change* — a possessed creature arriving in our hands, or our
|
||||
// own body coming back (decision 1277). It is one path deliberately: seizing a body means the
|
||||
// same thing either way, and the resource's pose describes whatever we were driving a moment
|
||||
// ago. What differs is only the login-once half below (`first`), which owns the settle, the
|
||||
// stale-world flag and the initial camera seat.
|
||||
let seizing = !player.active || player.reseat;
|
||||
if seizing {
|
||||
if let Some((pos, yaw)) = self_pose {
|
||||
let first = !player.active;
|
||||
player.reseat = false;
|
||||
// Momentum, movement bits and the platform under our feet all belonged to the body we
|
||||
// just let go of. The reference's `SetActiveMover 0x6006e0` tears exactly this down on
|
||||
// the outgoing mover — resetting movement flags and cancelling click-to-move — and
|
||||
// carrying any of it across would have the new body sprinting, falling or standing on a
|
||||
// boat it is nowhere near.
|
||||
if !first {
|
||||
player.vel_y = 0.0;
|
||||
player.horiz_vel = Vec3::ZERO;
|
||||
player.move_flags = 0;
|
||||
player.autorun = false;
|
||||
player.airborne_since = None;
|
||||
player.fall_far = false;
|
||||
player.fall_start_y = pos.y;
|
||||
player.ride = None;
|
||||
// The granted modes (root, water-walk, feather-fall, hover) were granted to the
|
||||
// *previous* mover; the new one's arrive on its own `SMSG_FORCE_*`/mode packets.
|
||||
player.modes = Default::default();
|
||||
}
|
||||
player.pos = pos;
|
||||
player.active = true;
|
||||
player.settling = true; // settle onto the initial ground once it loads (don't fall through)
|
||||
player.settle_deadline = time.elapsed_secs() + SETTLE_TIMEOUT;
|
||||
player.world_stale = true; // nothing is streamed yet at all — see [`Player::world_stale`]
|
||||
// The streamed spawn pose carries the server's facing (the character's logout
|
||||
// orientation on a fresh login) — adopt it whole, camera seated behind, like the
|
||||
// reference. Zeroing here is what made every login face due north regardless.
|
||||
cam.yaw = yaw;
|
||||
cam.pitch = -0.45;
|
||||
if first {
|
||||
player.settling = true; // settle onto the initial ground once it loads (don't fall through)
|
||||
player.settle_deadline = time.elapsed_secs() + SETTLE_TIMEOUT;
|
||||
player.world_stale = true; // nothing is streamed yet — see [`Player::world_stale`]
|
||||
// The streamed spawn pose carries the server's facing (the character's logout
|
||||
// orientation on a fresh login) — adopt it whole, camera seated behind, like
|
||||
// the reference. Zeroing here made every login face due north regardless.
|
||||
cam.yaw = yaw;
|
||||
cam.pitch = -0.45;
|
||||
}
|
||||
// The camera is deliberately NOT re-seated on a mover change: it is already on the new
|
||||
// body (Mind Control sets `PLAYER_FARSIGHT` to the victim alongside the handoff, so
|
||||
// far sight moved it there before the reins arrived), and snapping its yaw would spin
|
||||
// the view for a change the player made happen on purpose.
|
||||
player.face_yaw = yaw;
|
||||
player.model_yaw = yaw;
|
||||
// Seed the facing-change detector with the adopted facing — the server gave it to us,
|
||||
@@ -314,7 +407,11 @@ pub(super) fn apply_server_moves(
|
||||
// cross-map worldport despawns and re-streams this entity while `player.active` stays
|
||||
// true — per-entity state attached only on this one-shot edge would be lost on transfer.
|
||||
info!(
|
||||
"took control of player @ {:?} facing {:.3}{}",
|
||||
"took control of {} @ {:?} facing {:.3}{}",
|
||||
match player.foreign_mover {
|
||||
Some(guid) => format!("possessed unit {guid:#x}"),
|
||||
None => "player".to_string(),
|
||||
},
|
||||
player.pos,
|
||||
yaw.rem_euclid(std::f32::consts::TAU),
|
||||
crate::run_mode::free_fly_hint()
|
||||
@@ -392,7 +489,7 @@ fn apply_self_move(
|
||||
time: &Time,
|
||||
transports: &Query<
|
||||
(&Transform, &Guid),
|
||||
(With<Transport>, Without<SelfPlayer>, Without<FlyCam>),
|
||||
(With<Transport>, Without<ActiveMover>, Without<FlyCam>),
|
||||
>,
|
||||
) {
|
||||
let was_falling = player.move_flags & move_flags::FALLING != 0;
|
||||
|
||||
@@ -30,10 +30,12 @@ pub(super) fn publish_viewer(
|
||||
player: Option<Res<Player>>,
|
||||
rig: Option<Res<super::CameraControl>>,
|
||||
screen: Option<Res<crate::loading_screen::LoadingScreen>>,
|
||||
store: Query<&crate::net::ObjectStore, With<benilla_world::world_unit::ViewerUnit>>,
|
||||
store: Query<&crate::net::ObjectStore, With<crate::net::SelfPlayer>>,
|
||||
) {
|
||||
// The viewer's *condition*, off its own descriptor block: both are whole-screen effects keyed
|
||||
// on the eye's owner, which is why they ride here rather than on any body in the scene.
|
||||
// The viewer's *condition*, off **our own character's** descriptor block: both are whole-screen
|
||||
// effects, which is why they ride here rather than on any body in the scene — and why they read
|
||||
// `SelfPlayer` rather than the body we drive. Being drunk is a fact about you; possessing a boar
|
||||
// does not sober you up, and a boar has no drunk byte to read (decision 1277).
|
||||
let (drunk, ghost) = match store.single() {
|
||||
Ok(s) => (
|
||||
s.0.player_drunk_byte()
|
||||
|
||||
@@ -28,7 +28,7 @@ use bevy::prelude::*;
|
||||
use benilla_assets::coords::{bevy_to_wow, wow_to_bevy};
|
||||
use benilla_formats::WaterSoundCatalog;
|
||||
|
||||
use crate::net::SelfPlayer;
|
||||
use crate::net::ActiveMover;
|
||||
use benilla_assets::{AssetSet, LockRecover, WorldAssets};
|
||||
use benilla_world::schedule::WorldStage;
|
||||
|
||||
@@ -94,8 +94,8 @@ fn drive_liquid_loops(
|
||||
mut state: ResMut<LiquidLoopState>,
|
||||
water_sounds: Option<Res<WaterSounds>>,
|
||||
world: benilla_world::world_point::WorldPoint,
|
||||
player: Query<&Transform, With<SelfPlayer>>,
|
||||
mut emitters: Query<&mut Transform, Without<SelfPlayer>>,
|
||||
player: Query<&Transform, With<ActiveMover>>,
|
||||
mut emitters: Query<&mut Transform, Without<ActiveMover>>,
|
||||
time: Res<Time>,
|
||||
kits: Option<ResMut<SoundKits>>,
|
||||
assets: Option<Res<WorldAssets>>,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::net::SelfPlayer;
|
||||
use crate::net::ActiveMover;
|
||||
use crate::player::{head_height, CameraPivot, Player};
|
||||
use benilla_world::schedule::WorldStage;
|
||||
use benilla_world::view::WorldCamera;
|
||||
@@ -269,8 +269,8 @@ fn update_audio_listener(
|
||||
mut listener: ResMut<AudioListener>,
|
||||
mut out: NonSendMut<SoundOutput>,
|
||||
player: Res<Player>,
|
||||
self_av: Query<(&Transform, Option<&CameraPivot>), With<SelfPlayer>>,
|
||||
cam: Query<&Transform, (With<WorldCamera>, Without<SelfPlayer>)>,
|
||||
self_av: Query<(&Transform, Option<&CameraPivot>), With<ActiveMover>>,
|
||||
cam: Query<&Transform, (With<WorldCamera>, Without<ActiveMover>)>,
|
||||
) {
|
||||
// At-character (the default). `player.pos` is the feet; the head offset is the shared
|
||||
// model-derived pivot height, and the facing is the aim yaw about world-up (world +Y).
|
||||
|
||||
Reference in New Issue
Block a user