feat(net): handle 0xF74E VectorUpdate so remote players' jumps render
Remote-player jumps were silently dropped — we never parsed the
VectorUpdate broadcast that carries the jump launch velocity, so
the remote body's Z velocity stayed at 0 and the jump animation
showed without any vertical motion.
ACE Player.cs:954 enqueues GameMessageVectorUpdate (opcode 0xF74E)
on every jump in addition to the bracketing UpdateMotion. Wire
layout (GameMessageVectorUpdate.cs):
u32 opcode (= 0xF74E)
u32 objectGuid
3xf32 velocity (world-space, post-rotation)
3xf32 omega
u16 instanceSequence
u16 vectorSequence
This commit:
1. Adds VectorUpdate.TryParse + VectorUpdated session event.
2. WorldSession.ProcessDatagram dispatches 0xF74E.
3. GameWindow subscribes via OnLiveVectorUpdated:
- Sets remote PhysicsBody.Velocity from the wire vector.
- When velocity.Z > 0.5 m/s, marks the remote as Airborne,
clears Contact + OnWalkable bits, and enables the Gravity
state flag — so calc_acceleration returns (0, 0, -9.8) and
UpdatePhysicsInternal produces a parabolic arc.
4. The per-tick remote update (TickAnimations remote-physics
block) now SKIPS the "force OnWalkable + apply_current_movement"
step when Airborne. Otherwise that path stomps the +Z velocity
each frame — same shape as the bug the local jump hit before
K-fix7.
5. ResolveWithTransition for remotes now passes
isOnGround: !rm.Airborne. Mirrors K-fix7's local-player gate —
airborne resolves must NOT pre-seed the ContactPlane,
otherwise AdjustOffset's snap-to-plane branch zeroes the
upward offset.
6. UpdatePosition handler clears the airborne flag and restores
ground-contact bits, so the server's authoritative re-grounding
ends the arc cleanly at the new ground location.
ACDREAM_DUMP_MOTION=1 logs each VectorUpdate as
"VU guid=0x... vel=(...) airborne=...".
Tests stay 1222 green. Live verification pending — watch a remote
character jump.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1fce21034a
commit
b609b5ea6e
3 changed files with 190 additions and 5 deletions
|
|
@ -240,6 +240,18 @@ public sealed class GameWindow : IDisposable
|
|||
/// </summary>
|
||||
public uint CellId;
|
||||
|
||||
/// <summary>
|
||||
/// K-fix9 (2026-04-26): true while the remote is airborne (jump
|
||||
/// arc in flight). Set when a 0xF74E VectorUpdate arrives with
|
||||
/// non-trivial +Z velocity; cleared when the next UpdatePosition
|
||||
/// snaps to a new ground location. While true, the per-tick
|
||||
/// remote update SKIPS the "force OnWalkable + apply_current_movement"
|
||||
/// step that would otherwise stomp the body's Z velocity each
|
||||
/// frame, AND enables gravity so the parabolic arc actually plays
|
||||
/// out between server snaps.
|
||||
/// </summary>
|
||||
public bool Airborne;
|
||||
|
||||
public RemoteMotion()
|
||||
{
|
||||
Body = new AcDream.Core.Physics.PhysicsBody
|
||||
|
|
@ -1169,6 +1181,7 @@ public sealed class GameWindow : IDisposable
|
|||
_liveSession.EntitySpawned += OnLiveEntitySpawned;
|
||||
_liveSession.MotionUpdated += OnLiveMotionUpdated;
|
||||
_liveSession.PositionUpdated += OnLivePositionUpdated;
|
||||
_liveSession.VectorUpdated += OnLiveVectorUpdated;
|
||||
_liveSession.TeleportStarted += OnTeleportStarted;
|
||||
|
||||
// Phase 6c — PlayScript (0xF754) arrives from the server as
|
||||
|
|
@ -2337,6 +2350,45 @@ public sealed class GameWindow : IDisposable
|
|||
/// snap the player entity + controller, and return to InWorld. Also sends
|
||||
/// LoginComplete so the server knows the client has loaded the destination.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// K-fix9 (2026-04-26): handle 0xF74E VectorUpdate — ACE broadcasts
|
||||
/// this on remote-player JUMPS (Player.cs:954). The payload carries
|
||||
/// the world-space launch velocity. Without handling it, remote
|
||||
/// jumps render as a tiny lift-and-back because we never see the
|
||||
/// +Z velocity that would integrate into a proper arc.
|
||||
/// </summary>
|
||||
private void OnLiveVectorUpdated(AcDream.Core.Net.Messages.VectorUpdate.Parsed update)
|
||||
{
|
||||
if (update.Guid == _playerServerGuid) return; // local jump uses our own physics
|
||||
if (!_remoteDeadReckon.TryGetValue(update.Guid, out var rm)) return;
|
||||
|
||||
// World-space velocity. Apply directly to the body — the per-tick
|
||||
// remote update will integrate Position += Velocity × dt + 0.5 × Accel × dt².
|
||||
rm.Body.Velocity = update.Velocity;
|
||||
|
||||
// Mark airborne when the launch has meaningful +Z. Threshold
|
||||
// 0.5 m/s rejects noise / horizontal-only updates (server might
|
||||
// also use VectorUpdate for non-jump events). The per-tick
|
||||
// remote update reads .Airborne to skip the ground-clamp branch
|
||||
// and apply gravity instead.
|
||||
if (update.Velocity.Z > 0.5f)
|
||||
{
|
||||
rm.Airborne = true;
|
||||
// Clear ground-contact bits + enable gravity so calc_acceleration
|
||||
// returns (0, 0, -9.8) instead of zero. UpdatePhysicsInternal then
|
||||
// produces the parabolic arc.
|
||||
rm.Body.TransientState &= ~(AcDream.Core.Physics.TransientStateFlags.Contact
|
||||
| AcDream.Core.Physics.TransientStateFlags.OnWalkable);
|
||||
rm.Body.State |= AcDream.Core.Physics.PhysicsStateFlags.Gravity;
|
||||
}
|
||||
|
||||
if (Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1")
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"VU guid=0x{update.Guid:X8} vel=({update.Velocity.X:F2},{update.Velocity.Y:F2},{update.Velocity.Z:F2}) airborne={rm.Airborne}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLivePositionUpdated(AcDream.Core.Net.WorldSession.EntityPositionUpdate update)
|
||||
{
|
||||
// Phase A.1: track the most recently updated entity's landblock so the
|
||||
|
|
@ -2407,6 +2459,20 @@ public sealed class GameWindow : IDisposable
|
|||
rmState.Body.Orientation = rot;
|
||||
}
|
||||
rmState.Body.Position = worldPos;
|
||||
// K-fix9 (2026-04-26): UpdatePosition is the server's
|
||||
// authoritative re-grounding signal after a jump. Clear the
|
||||
// airborne flag, restore Contact + OnWalkable, and disable
|
||||
// gravity so the next per-tick remote update goes back to
|
||||
// the regular ground-clamped path. The server typically
|
||||
// sends a UP at the apex / mid-arc / land — our integration
|
||||
// fills in between.
|
||||
if (rmState.Airborne)
|
||||
{
|
||||
rmState.Airborne = false;
|
||||
rmState.Body.TransientState |= AcDream.Core.Physics.TransientStateFlags.Contact
|
||||
| AcDream.Core.Physics.TransientStateFlags.OnWalkable;
|
||||
rmState.Body.State &= ~AcDream.Core.Physics.PhysicsStateFlags.Gravity;
|
||||
}
|
||||
// Adopt the server's cell ID as the transition starting cell.
|
||||
// Retail authoritatively hard-snaps cell membership here too; our
|
||||
// per-tick ResolveWithTransition sweep then advances CheckCellId
|
||||
|
|
@ -4490,10 +4556,28 @@ public sealed class GameWindow : IDisposable
|
|||
// Forces OnWalkable + Contact so the gate in apply_current_movement
|
||||
// always succeeds (remotes are server-authoritative; we don't
|
||||
// simulate airborne physics for them).
|
||||
rm.Body.TransientState |= AcDream.Core.Physics.TransientStateFlags.Contact
|
||||
| AcDream.Core.Physics.TransientStateFlags.OnWalkable
|
||||
| AcDream.Core.Physics.TransientStateFlags.Active;
|
||||
rm.Motion.apply_current_movement(cancelMoveTo: false, allowJump: false);
|
||||
//
|
||||
// K-fix9 (2026-04-26): SKIP this when the remote is airborne.
|
||||
// Otherwise the force-OnWalkable + apply_current_movement
|
||||
// path stomps the +Z velocity we set in OnLiveVectorUpdated,
|
||||
// and gravity never gets to integrate the arc. The airborne
|
||||
// body keeps the launch velocity from the VectorUpdate;
|
||||
// UpdatePhysicsInternal below applies gravity each tick;
|
||||
// the next UpdatePosition snaps to the new ground location
|
||||
// and re-grounds.
|
||||
if (!rm.Airborne)
|
||||
{
|
||||
rm.Body.TransientState |= AcDream.Core.Physics.TransientStateFlags.Contact
|
||||
| AcDream.Core.Physics.TransientStateFlags.OnWalkable
|
||||
| AcDream.Core.Physics.TransientStateFlags.Active;
|
||||
rm.Motion.apply_current_movement(cancelMoveTo: false, allowJump: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Airborne — keep Active flag (so UpdatePhysicsInternal
|
||||
// doesn't early-return) but DON'T set Contact / OnWalkable.
|
||||
rm.Body.TransientState |= AcDream.Core.Physics.TransientStateFlags.Active;
|
||||
}
|
||||
|
||||
// Step 2: integrate rotation manually per tick. We can't
|
||||
// rely on PhysicsBody.update_object here — its MinQuantum
|
||||
|
|
@ -4573,7 +4657,12 @@ public sealed class GameWindow : IDisposable
|
|||
sphereHeight: 1.2f,
|
||||
stepUpHeight: 2.0f, // retail default for unknown remotes
|
||||
stepDownHeight: 0.04f, // PhysicsGlobals.DefaultStepHeight
|
||||
isOnGround: true, // remotes are forced OnWalkable above
|
||||
// K-fix9 (2026-04-26): mirror the K-fix7 gate —
|
||||
// airborne remotes must NOT pre-seed the
|
||||
// ContactPlane, otherwise AdjustOffset's snap-to-plane
|
||||
// branch zeroes the +Z offset every step (same bug
|
||||
// we hit on the local jump).
|
||||
isOnGround: !rm.Airborne,
|
||||
body: rm.Body); // persist ContactPlane across frames for slope tracking
|
||||
|
||||
rm.Body.Position = resolveResult.Position;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue