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
39
src/AcDream.Core.Net/Messages/DeleteObject.cs
Normal file
39
src/AcDream.Core.Net/Messages/DeleteObject.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
using System.Buffers.Binary;
|
||||
|
||||
namespace AcDream.Core.Net.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Inbound <c>ObjectDelete</c> GameMessage (opcode <c>0xF747</c>).
|
||||
///
|
||||
/// <para>
|
||||
/// Retail dispatch path:
|
||||
/// <c>CM_Physics::DispatchSB_DeleteObject</c> 0x006AC6A0 reads guid from
|
||||
/// <c>buf+4</c> and instance sequence from <c>buf+8</c>, then calls
|
||||
/// <c>SmartBox::HandleDeleteObject</c> 0x00451EA0. ACE emits the same
|
||||
/// layout from <c>GameMessageDeleteObject</c>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class DeleteObject
|
||||
{
|
||||
public const uint Opcode = 0xF747u;
|
||||
|
||||
public readonly record struct Parsed(uint Guid, ushort InstanceSequence);
|
||||
|
||||
/// <summary>
|
||||
/// Parse a 0xF747 body. <paramref name="body"/> must start with the
|
||||
/// 4-byte opcode, matching every other parser in this namespace.
|
||||
/// </summary>
|
||||
public static Parsed? TryParse(ReadOnlySpan<byte> body)
|
||||
{
|
||||
if (body.Length < 10)
|
||||
return null;
|
||||
|
||||
uint opcode = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(0, 4));
|
||||
if (opcode != Opcode)
|
||||
return null;
|
||||
|
||||
uint guid = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(4, 4));
|
||||
ushort instanceSequence = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(8, 2));
|
||||
return new Parsed(guid, instanceSequence);
|
||||
}
|
||||
}
|
||||
|
|
@ -61,6 +61,16 @@ public sealed class WorldSession : IDisposable
|
|||
/// <summary>Fires when the session finishes parsing a CreateObject.</summary>
|
||||
public event Action<EntitySpawn>? EntitySpawned;
|
||||
|
||||
/// <summary>
|
||||
/// Fires when the session parses a 0xF747 ObjectDelete game message.
|
||||
/// Retail routes this through
|
||||
/// <c>CM_Physics::DispatchSB_DeleteObject</c> 0x006AC6A0 →
|
||||
/// <c>SmartBox::HandleDeleteObject</c> 0x00451EA0; ACE emits it when
|
||||
/// an object leaves the world, including the living creature object
|
||||
/// after its corpse is created.
|
||||
/// </summary>
|
||||
public event Action<DeleteObject.Parsed>? EntityDeleted;
|
||||
|
||||
/// <summary>
|
||||
/// Payload for <see cref="MotionUpdated"/>: the server guid of the entity
|
||||
/// whose motion changed and its new server-side stance + forward command.
|
||||
|
|
@ -641,6 +651,12 @@ public sealed class WorldSession : IDisposable
|
|||
parsed.Value.MotionTableId));
|
||||
}
|
||||
}
|
||||
else if (op == DeleteObject.Opcode)
|
||||
{
|
||||
var parsed = DeleteObject.TryParse(body);
|
||||
if (parsed is not null)
|
||||
EntityDeleted?.Invoke(parsed.Value);
|
||||
}
|
||||
else if (op == UpdateMotion.Opcode)
|
||||
{
|
||||
// Phase 6.6: the server sends UpdateMotion (0xF74C) whenever an
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue