fix(anim): Phase L.1c route creature actions and despawns
Handle retail ObjectDelete (0xF747) using CM_Physics::DispatchSB_DeleteObject 0x006AC6A0 / SmartBox::HandleDeleteObject 0x00451EA0 and ACE GameMessageDeleteObject so dead creatures are removed when corpses spawn. Route action-class ForwardCommand values through AnimationCommandRouter/PlayAction instead of SetCycle so creature attack commands 0x51/0x52/0x53 survive the immediate Ready echo, matching CMotionTable::GetObjectSequence 0x00522860 / ACE MotionTable.GetObjectSequence. Use server-authoritative UpdatePosition velocity, or observed server position delta for non-player entities when HasVelocity is absent, to reduce monster/NPC chase lag without applying player RUM prediction to server-controlled creatures.
This commit is contained in:
parent
4874d8595a
commit
b96b680a20
5 changed files with 235 additions and 21 deletions
|
|
@ -216,6 +216,14 @@ public sealed class GameWindow : IDisposable
|
|||
/// <summary>Last known server position — kept for diagnostics / HUD.</summary>
|
||||
public System.Numerics.Vector3 LastServerPos;
|
||||
/// <summary>
|
||||
/// Latest server-authoritative velocity for NPC/monster smoothing.
|
||||
/// Prefer the HasVelocity vector from UpdatePosition; when ACE omits
|
||||
/// it for a server-controlled creature, derive it from consecutive
|
||||
/// authoritative positions instead of guessing from player RUM state.
|
||||
/// </summary>
|
||||
public System.Numerics.Vector3 ServerVelocity;
|
||||
public bool HasServerVelocity;
|
||||
/// <summary>
|
||||
/// Legacy field — no longer used for slerp (retail hard-snaps
|
||||
/// per FUN_00514b90 set_frame). Kept to avoid churn.
|
||||
/// </summary>
|
||||
|
|
@ -527,6 +535,7 @@ public sealed class GameWindow : IDisposable
|
|||
private readonly record struct LiveEntityInfo(
|
||||
string? Name,
|
||||
AcDream.Core.Items.ItemType ItemType);
|
||||
private static bool IsPlayerGuid(uint guid) => (guid & 0xFF000000u) == 0x50000000u;
|
||||
private int _liveSpawnReceived; // diagnostics
|
||||
private int _liveSpawnHydrated;
|
||||
private int _liveDropReasonNoPos;
|
||||
|
|
@ -1303,6 +1312,7 @@ public sealed class GameWindow : IDisposable
|
|||
Console.WriteLine($"live: connecting to {endpoint} as {user}");
|
||||
_liveSession = new AcDream.Core.Net.WorldSession(endpoint);
|
||||
_liveSession.EntitySpawned += OnLiveEntitySpawned;
|
||||
_liveSession.EntityDeleted += OnLiveEntityDeleted;
|
||||
_liveSession.MotionUpdated += OnLiveMotionUpdated;
|
||||
_liveSession.PositionUpdated += OnLivePositionUpdated;
|
||||
_liveSession.VectorUpdated += OnLiveVectorUpdated;
|
||||
|
|
@ -1654,23 +1664,7 @@ public sealed class GameWindow : IDisposable
|
|||
// For a respawn, drop the previous rendering state here before we
|
||||
// build the new one. `_entitiesByServerGuid` is the canonical map,
|
||||
// its value is the live WorldEntity we need to dispose.
|
||||
if (_entitiesByServerGuid.TryGetValue(spawn.Guid, out var existingEntity))
|
||||
{
|
||||
_worldState.RemoveEntityByServerGuid(spawn.Guid);
|
||||
_worldGameState.RemoveById(existingEntity.Id);
|
||||
_animatedEntities.Remove(existingEntity.Id);
|
||||
// Physics collision registry entry is keyed by local id too.
|
||||
_physicsEngine.ShadowObjects.Deregister(existingEntity.Id);
|
||||
// Dead-reckon state is keyed by SERVER guid (not local id) so we
|
||||
// clear using the same guid the new spawn will use. Leaving old
|
||||
// SnapResidual / DeadReckonedPos in would make the next first
|
||||
// UpdatePosition look like a 2m-residual soft-snap.
|
||||
_remoteDeadReckon.Remove(spawn.Guid);
|
||||
_remoteLastMove.Remove(spawn.Guid);
|
||||
_liveEntityInfoByGuid.Remove(spawn.Guid);
|
||||
if (_selectedTargetGuid == spawn.Guid)
|
||||
_selectedTargetGuid = null;
|
||||
}
|
||||
RemoveLiveEntityByServerGuid(spawn.Guid, logDelete: false);
|
||||
|
||||
// Log every spawn that arrives so we can inventory what the server
|
||||
// sends (including the ones we can't render yet). The Name field
|
||||
|
|
@ -2150,6 +2144,41 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private void OnLiveEntityDeleted(AcDream.Core.Net.Messages.DeleteObject.Parsed delete)
|
||||
{
|
||||
if (RemoveLiveEntityByServerGuid(delete.Guid, logDelete: true)
|
||||
&& Environment.GetEnvironmentVariable("ACDREAM_DUMP_MOTION") == "1")
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"live: delete guid=0x{delete.Guid:X8} instSeq={delete.InstanceSequence}");
|
||||
}
|
||||
}
|
||||
|
||||
private bool RemoveLiveEntityByServerGuid(uint serverGuid, bool logDelete)
|
||||
{
|
||||
if (!_entitiesByServerGuid.TryGetValue(serverGuid, out var existingEntity))
|
||||
return false;
|
||||
|
||||
_worldState.RemoveEntityByServerGuid(serverGuid);
|
||||
_worldGameState.RemoveById(existingEntity.Id);
|
||||
_animatedEntities.Remove(existingEntity.Id);
|
||||
_physicsEngine.ShadowObjects.Deregister(existingEntity.Id);
|
||||
|
||||
// Dead-reckon state is keyed by SERVER guid (not local id) so we
|
||||
// clear using the same guid the next spawn/update would use.
|
||||
_remoteDeadReckon.Remove(serverGuid);
|
||||
_remoteLastMove.Remove(serverGuid);
|
||||
_liveEntityInfoByGuid.Remove(serverGuid);
|
||||
_entitiesByServerGuid.Remove(serverGuid);
|
||||
if (_selectedTargetGuid == serverGuid)
|
||||
_selectedTargetGuid = null;
|
||||
|
||||
if (logDelete)
|
||||
_lightingSink?.UnregisterOwner(existingEntity.Id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase 6.6: the server says an entity's motion has changed. Look up
|
||||
/// the AnimatedEntity for that guid, re-resolve the idle cycle with the
|
||||
|
|
@ -2293,6 +2322,32 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
else
|
||||
{
|
||||
var forwardRoute = AcDream.Core.Physics.AnimationCommandRouter.Classify(fullMotion);
|
||||
bool forwardIsOverlay = forwardRoute is AcDream.Core.Physics.AnimationCommandRouteKind.Action
|
||||
or AcDream.Core.Physics.AnimationCommandRouteKind.Modifier
|
||||
or AcDream.Core.Physics.AnimationCommandRouteKind.ChatEmote;
|
||||
bool remoteIsAirborne = _remoteDeadReckon.TryGetValue(update.Guid, out var rmCheck)
|
||||
&& rmCheck.Airborne;
|
||||
|
||||
// Retail MotionTable::GetObjectSequence routes action-class
|
||||
// ForwardCommand values (creature attacks, chat-emotes) through
|
||||
// the Action branch, where the swing is appended before the
|
||||
// current cyclic tail and currState.Substate remains Ready.
|
||||
// Treating 0x10000051/52/53 as SetCycle commands made the
|
||||
// immediate follow-up Ready packet abort the swing.
|
||||
if (forwardIsOverlay)
|
||||
{
|
||||
if (!remoteIsAirborne)
|
||||
{
|
||||
AcDream.Core.Physics.AnimationCommandRouter.RouteFullCommand(
|
||||
ae.Sequencer,
|
||||
fullStyle,
|
||||
fullMotion,
|
||||
speedMod <= 0f ? 1f : speedMod);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Pick which cycle to play on the sequencer. Priority:
|
||||
// 1. Forward cmd if active (RunForward / WalkForward) — legs run/walk.
|
||||
// 2. Else sidestep cmd if active — legs strafe.
|
||||
|
|
@ -2340,8 +2395,6 @@ public sealed class GameWindow : IDisposable
|
|||
// the post-resolve landing path restores the cycle to
|
||||
// whatever the interpreted state says when the body
|
||||
// lands.
|
||||
bool remoteIsAirborne = _remoteDeadReckon.TryGetValue(update.Guid, out var rmCheck)
|
||||
&& rmCheck.Airborne;
|
||||
if (!remoteIsAirborne)
|
||||
ae.Sequencer.SetCycle(fullStyle, animCycle, animSpeed);
|
||||
|
||||
|
|
@ -2419,6 +2472,7 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL: when we enter a locomotion cycle (Walk/Run/etc),
|
||||
// stamp the _remoteLastMove timestamp to "now". Without this,
|
||||
|
|
@ -2634,6 +2688,26 @@ public sealed class GameWindow : IDisposable
|
|||
// slerp doesn't visibly rotate from Identity to truth.
|
||||
rmState.Body.Orientation = rot;
|
||||
}
|
||||
double nowSec = (now - System.DateTime.UnixEpoch).TotalSeconds;
|
||||
System.Numerics.Vector3? serverVelocity = update.Velocity;
|
||||
if (serverVelocity is null
|
||||
&& !IsPlayerGuid(update.Guid)
|
||||
&& rmState.LastServerPosTime > 0.0)
|
||||
{
|
||||
double elapsed = nowSec - rmState.LastServerPosTime;
|
||||
if (elapsed > 0.001)
|
||||
serverVelocity = (worldPos - rmState.LastServerPos) / (float)elapsed;
|
||||
}
|
||||
if (serverVelocity is { } authoritativeVelocity)
|
||||
{
|
||||
rmState.ServerVelocity = authoritativeVelocity;
|
||||
rmState.HasServerVelocity = true;
|
||||
}
|
||||
else if (!IsPlayerGuid(update.Guid))
|
||||
{
|
||||
rmState.ServerVelocity = System.Numerics.Vector3.Zero;
|
||||
rmState.HasServerVelocity = false;
|
||||
}
|
||||
rmState.Body.Position = worldPos;
|
||||
// K-fix15 (2026-04-26): DON'T auto-clear airborne on UP.
|
||||
// ACE broadcasts UPs during the arc (peak / mid-fall / land)
|
||||
|
|
@ -2673,7 +2747,7 @@ public sealed class GameWindow : IDisposable
|
|||
rmState.Body.Orientation = rot;
|
||||
rmState.TargetOrientation = rot;
|
||||
rmState.LastServerPos = worldPos;
|
||||
rmState.LastServerPosTime = (now - System.DateTime.UnixEpoch).TotalSeconds;
|
||||
rmState.LastServerPosTime = nowSec;
|
||||
// Align the body's physics clock with our clock so update_object
|
||||
// doesn't sub-step a huge initial gap.
|
||||
rmState.Body.LastUpdateTime = rmState.LastServerPosTime;
|
||||
|
|
@ -2710,6 +2784,10 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
}
|
||||
else if (!IsPlayerGuid(update.Guid) && rmState.HasServerVelocity)
|
||||
{
|
||||
rmState.Body.Velocity = rmState.ServerVelocity;
|
||||
}
|
||||
|
||||
entity.Position = rmState.Body.Position;
|
||||
entity.Rotation = rmState.Body.Orientation;
|
||||
|
|
@ -4858,7 +4936,10 @@ public sealed class GameWindow : IDisposable
|
|||
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);
|
||||
if (!IsPlayerGuid(serverGuid) && rm.HasServerVelocity)
|
||||
rm.Body.Velocity = rm.ServerVelocity;
|
||||
else
|
||||
rm.Motion.apply_current_movement(cancelMoveTo: false, allowJump: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue