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
tests/AcDream.Core.Net.Tests/Messages/DeleteObjectTests.cs
Normal file
39
tests/AcDream.Core.Net.Tests/Messages/DeleteObjectTests.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
using System.Buffers.Binary;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Net.Tests.Messages;
|
||||
|
||||
public sealed class DeleteObjectTests
|
||||
{
|
||||
[Fact]
|
||||
public void RejectsWrongOpcode()
|
||||
{
|
||||
Span<byte> body = stackalloc byte[12];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body, 0xDEADBEEFu);
|
||||
|
||||
Assert.Null(DeleteObject.TryParse(body));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RejectsTruncated()
|
||||
{
|
||||
Assert.Null(DeleteObject.TryParse(ReadOnlySpan<byte>.Empty));
|
||||
Assert.Null(DeleteObject.TryParse(new byte[9]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesGuidAndInstanceSequence()
|
||||
{
|
||||
Span<byte> body = stackalloc byte[12];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body, DeleteObject.Opcode);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(body.Slice(4), 0x80000439u);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(body.Slice(8), 0x1234);
|
||||
|
||||
var parsed = DeleteObject.TryParse(body);
|
||||
|
||||
Assert.NotNull(parsed);
|
||||
Assert.Equal(0x80000439u, parsed!.Value.Guid);
|
||||
Assert.Equal((ushort)0x1234, parsed.Value.InstanceSequence);
|
||||
}
|
||||
}
|
||||
|
|
@ -1313,6 +1313,45 @@ public sealed class AnimationSequencerTests
|
|||
Assert.Equal(99f, fr[0].Origin.X, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlayAction_ActionSurvivesImmediateReadyCycleEcho()
|
||||
{
|
||||
// ACE broadcasts creature attacks as Action-class ForwardCommand
|
||||
// values followed by Ready. Retail keeps currState.Substate at Ready
|
||||
// while the action link drains, so the Ready echo must not abort the
|
||||
// in-flight swing.
|
||||
const uint Style = 0x003Du;
|
||||
const uint IdleMotion = 0x41000003u;
|
||||
const uint AttackMotion = 0x10000052u;
|
||||
const uint IdleAnimId = 0x03000503u;
|
||||
const uint AttackAnimId = 0x03000504u;
|
||||
|
||||
var setup = Fixtures.MakeSetup(1);
|
||||
var mt = new MotionTable { DefaultStyle = (DRWMotionCommand)Style };
|
||||
int cycleKey = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu));
|
||||
mt.Cycles[cycleKey] = Fixtures.MakeMotionData(IdleAnimId, framerate: 10f);
|
||||
|
||||
int linkOuter = (int)((Style << 16) | (IdleMotion & 0xFFFFFFu));
|
||||
var cmdData = new MotionCommandData();
|
||||
cmdData.MotionData[(int)AttackMotion] = Fixtures.MakeMotionData(AttackAnimId, framerate: 10f);
|
||||
mt.Links[linkOuter] = cmdData;
|
||||
|
||||
var loader = new FakeLoader();
|
||||
loader.Register(IdleAnimId, Fixtures.MakeAnim(4, 1, Vector3.Zero, Quaternion.Identity));
|
||||
loader.Register(AttackAnimId, Fixtures.MakeAnim(3, 1, new Vector3(12, 0, 0), Quaternion.Identity));
|
||||
|
||||
var seq = new AnimationSequencer(setup, mt, loader);
|
||||
seq.SetCycle(Style, IdleMotion);
|
||||
seq.PlayAction(AttackMotion);
|
||||
|
||||
seq.SetCycle(Style, IdleMotion);
|
||||
|
||||
var fr = seq.Advance(0.01f);
|
||||
Assert.Single(fr);
|
||||
Assert.Equal(12f, fr[0].Origin.X, 1);
|
||||
Assert.Equal(IdleMotion, seq.CurrentMotion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlayAction_Modifier_ResolvesFromModifiersDict()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue