research(vfx): pin retail projectile and effect oracle
Establish the executable-backed PhysicsDesc, sequence-gate, PhysicsScript, CreateBlocking, particle-anchor, projectile, and Hidden-state behavior before changing runtime code. Correct stale blocking/threshold claims and synchronize the project instructions with the current UI architecture and matching retail binary. Add copyright-safe packet and DAT-container fixtures plus a failing installed-DAT conformance audit for projectile shapes, typed tables, recall motion, default scripts, and raw CreateBlocking inventory. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
parent
bb5acab9e6
commit
d53fe30ffe
14 changed files with 1545 additions and 53 deletions
135
tests/AcDream.Content.Tests/Vfx/ProjectileVfxDatFixtures.cs
Normal file
135
tests/AcDream.Content.Tests/Vfx/ProjectileVfxDatFixtures.cs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
using System.Buffers.Binary;
|
||||
|
||||
namespace AcDream.Content.Tests.Vfx;
|
||||
|
||||
/// <summary>
|
||||
/// Synthetic raw DAT fixtures captured from the retail hook schema audit.
|
||||
/// They contain no installed Turbine DAT bytes. Step 3 consumes the blocking
|
||||
/// hook followed by an ordinary hook to prove cursor alignment.
|
||||
/// </summary>
|
||||
internal static class ProjectileVfxDatFixtures
|
||||
{
|
||||
internal static readonly byte[] PhysicsScriptCreateBlockingThenAnimationDone = BuildPhysicsScript();
|
||||
internal static readonly byte[] AnimationCreateBlockingThenAnimationDone = BuildAnimation();
|
||||
|
||||
private static byte[] BuildPhysicsScript()
|
||||
{
|
||||
var bytes = new byte[80];
|
||||
int pos = 0;
|
||||
|
||||
WriteU32(0x3300F001u); // synthetic PhysicsScript DID
|
||||
WriteU32(2); // two PhysicsScriptData entries
|
||||
|
||||
WriteF64(0.0);
|
||||
WriteU32(0x1Au); // CreateBlockingParticle
|
||||
WriteU32(0); // AnimationHookDir.Both
|
||||
WriteU32(0x3200F001u); // emitter DID
|
||||
WriteU32(0xFFFFFFFFu); // root attachment, distinct from part zero
|
||||
WriteF32(1.0f); WriteF32(2.0f); WriteF32(3.0f); // offset origin
|
||||
WriteF32(0.5f); WriteF32(0.5f); WriteF32(-0.5f); WriteF32(-0.5f); // WXYZ quaternion
|
||||
WriteU32(7); // nonzero logical emitter ID
|
||||
|
||||
WriteF64(1.25);
|
||||
WriteU32(0x04u); // AnimationDone: proves the next cursor
|
||||
WriteU32(1); // AnimationHookDir.Forward
|
||||
|
||||
if (pos != bytes.Length) throw new InvalidOperationException($"Fixture length mismatch: {pos}");
|
||||
return bytes;
|
||||
|
||||
void WriteU32(uint value)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(pos), value);
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
void WriteF32(float value)
|
||||
{
|
||||
BinaryPrimitives.WriteSingleLittleEndian(bytes.AsSpan(pos), value);
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
void WriteF64(double value)
|
||||
{
|
||||
BinaryPrimitives.WriteDoubleLittleEndian(bytes.AsSpan(pos), value);
|
||||
pos += 8;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] BuildAnimation()
|
||||
{
|
||||
// Animation header: DID, flags, NumParts=0, NumFrames=1. The sole
|
||||
// AnimationFrame then has hookCount=2 and the same two hook records as
|
||||
// the PhysicsScript fixture. No position or part frames are present.
|
||||
var bytes = new byte[76];
|
||||
int pos = 0;
|
||||
|
||||
WriteU32(0x0300F001u); // synthetic Animation DID
|
||||
WriteU32(0); // no PosFrames
|
||||
WriteU32(0); // NumParts
|
||||
WriteU32(1); // NumFrames
|
||||
WriteU32(2); // AnimationFrame hook count
|
||||
|
||||
WriteU32(0x1Au); // CreateBlockingParticle
|
||||
WriteU32(0); // AnimationHookDir.Both
|
||||
WriteU32(0x3200F002u); // emitter DID
|
||||
WriteU32(0); // part zero, distinct from the PES root fixture
|
||||
WriteF32(-1.0f); WriteF32(4.0f); WriteF32(0.25f); // offset origin
|
||||
WriteF32(1.0f); WriteF32(0.0f); WriteF32(0.0f); WriteF32(0.0f); // WXYZ identity
|
||||
WriteU32(9); // nonzero logical emitter ID
|
||||
|
||||
WriteU32(0x04u); // AnimationDone at the exact next cursor
|
||||
WriteU32(0xFFFFFFFFu); // AnimationHookDir.Backward
|
||||
|
||||
if (pos != bytes.Length) throw new InvalidOperationException($"Fixture length mismatch: {pos}");
|
||||
return bytes;
|
||||
|
||||
void WriteU32(uint value)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(pos), value);
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
void WriteF32(float value)
|
||||
{
|
||||
BinaryPrimitives.WriteSingleLittleEndian(bytes.AsSpan(pos), value);
|
||||
pos += 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ProjectileVfxDatFixtureTests
|
||||
{
|
||||
[Fact]
|
||||
public void BlockingThenOrdinaryHook_PinsInheritedPayloadAndNextCursor()
|
||||
{
|
||||
ReadOnlySpan<byte> bytes = ProjectileVfxDatFixtures.PhysicsScriptCreateBlockingThenAnimationDone;
|
||||
|
||||
Assert.Equal(80, bytes.Length);
|
||||
Assert.Equal(0x3300F001u, BinaryPrimitives.ReadUInt32LittleEndian(bytes));
|
||||
Assert.Equal(2u, BinaryPrimitives.ReadUInt32LittleEndian(bytes[4..]));
|
||||
Assert.Equal(0x1Au, BinaryPrimitives.ReadUInt32LittleEndian(bytes[16..]));
|
||||
Assert.Equal(0x3200F001u, BinaryPrimitives.ReadUInt32LittleEndian(bytes[24..]));
|
||||
Assert.Equal(0xFFFFFFFFu, BinaryPrimitives.ReadUInt32LittleEndian(bytes[28..]));
|
||||
Assert.Equal(7u, BinaryPrimitives.ReadUInt32LittleEndian(bytes[60..]));
|
||||
Assert.Equal(1.25, BinaryPrimitives.ReadDoubleLittleEndian(bytes[64..]));
|
||||
Assert.Equal(0x04u, BinaryPrimitives.ReadUInt32LittleEndian(bytes[72..]));
|
||||
Assert.Equal(1u, BinaryPrimitives.ReadUInt32LittleEndian(bytes[76..]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnimationContainer_BlockingThenOrdinaryHookPinsNextCursor()
|
||||
{
|
||||
ReadOnlySpan<byte> bytes = ProjectileVfxDatFixtures.AnimationCreateBlockingThenAnimationDone;
|
||||
|
||||
Assert.Equal(76, bytes.Length);
|
||||
Assert.Equal(0x0300F001u, BinaryPrimitives.ReadUInt32LittleEndian(bytes));
|
||||
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(bytes[8..]));
|
||||
Assert.Equal(1u, BinaryPrimitives.ReadUInt32LittleEndian(bytes[12..]));
|
||||
Assert.Equal(2u, BinaryPrimitives.ReadUInt32LittleEndian(bytes[16..]));
|
||||
Assert.Equal(0x1Au, BinaryPrimitives.ReadUInt32LittleEndian(bytes[20..]));
|
||||
Assert.Equal(0x3200F002u, BinaryPrimitives.ReadUInt32LittleEndian(bytes[28..]));
|
||||
Assert.Equal(9u, BinaryPrimitives.ReadUInt32LittleEndian(bytes[64..]));
|
||||
Assert.Equal(0x04u, BinaryPrimitives.ReadUInt32LittleEndian(bytes[68..]));
|
||||
Assert.Equal(0xFFFFFFFFu, BinaryPrimitives.ReadUInt32LittleEndian(bytes[72..]));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,283 @@
|
|||
using System.Buffers.Binary;
|
||||
using AcDream.Core.Net.Messages;
|
||||
|
||||
namespace AcDream.Core.Net.Tests.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Synthetic, non-DAT packet fixtures captured by the Step 0 retail wire
|
||||
/// audit. Parser conformance tests consume these from Step 1 onward.
|
||||
/// </summary>
|
||||
internal static class ProjectileVfxPacketFixtures
|
||||
{
|
||||
internal static readonly byte[] DirectF754 =
|
||||
[
|
||||
0x54, 0xF7, 0x00, 0x00,
|
||||
0x42, 0x00, 0x00, 0x50,
|
||||
0x99, 0x00, 0x00, 0x33,
|
||||
];
|
||||
|
||||
internal static readonly byte[] TypedF755UnknownNaN =
|
||||
[
|
||||
0x55, 0xF7, 0x00, 0x00,
|
||||
0x42, 0x00, 0x00, 0x50,
|
||||
0xA5, 0xA5, 0xA5, 0xA5,
|
||||
0x34, 0x12, 0xC0, 0x7F,
|
||||
];
|
||||
|
||||
internal static readonly byte[] GoldenCreateObjectProjectilePhysicsFields = BuildGoldenCreateObject();
|
||||
internal static readonly byte[] CreateObjectMovementPresentEmpty = BuildBranchFixture(
|
||||
CreateObject.PhysicsDescriptionFlag.Movement, 0x50000051u);
|
||||
internal static readonly byte[] CreateObjectAnimationFramePresentZero = BuildBranchFixture(
|
||||
CreateObject.PhysicsDescriptionFlag.AnimationFrame, 0x50000052u);
|
||||
internal static readonly byte[] CreateObjectPeTablePresentZero = BuildBranchFixture(
|
||||
CreateObject.PhysicsDescriptionFlag.PeTable, 0x50000053u);
|
||||
internal static readonly byte[] CreateObjectPeTableAbsent = BuildBranchFixture(
|
||||
CreateObject.PhysicsDescriptionFlag.None, 0x50000054u);
|
||||
|
||||
private static byte[] BuildGoldenCreateObject()
|
||||
{
|
||||
var bytes = new List<byte>();
|
||||
uint flags =
|
||||
(uint)(CreateObject.PhysicsDescriptionFlag.CSetup
|
||||
| CreateObject.PhysicsDescriptionFlag.MTable
|
||||
| CreateObject.PhysicsDescriptionFlag.Velocity
|
||||
| CreateObject.PhysicsDescriptionFlag.Acceleration
|
||||
| CreateObject.PhysicsDescriptionFlag.Omega
|
||||
| CreateObject.PhysicsDescriptionFlag.Parent
|
||||
| CreateObject.PhysicsDescriptionFlag.Children
|
||||
| CreateObject.PhysicsDescriptionFlag.ObjScale
|
||||
| CreateObject.PhysicsDescriptionFlag.Friction
|
||||
| CreateObject.PhysicsDescriptionFlag.Elasticity
|
||||
| CreateObject.PhysicsDescriptionFlag.STable
|
||||
| CreateObject.PhysicsDescriptionFlag.PeTable
|
||||
| CreateObject.PhysicsDescriptionFlag.DefaultScript
|
||||
| CreateObject.PhysicsDescriptionFlag.DefaultScriptIntensity
|
||||
| CreateObject.PhysicsDescriptionFlag.Position
|
||||
| CreateObject.PhysicsDescriptionFlag.Translucency);
|
||||
|
||||
WriteU32(CreateObject.Opcode);
|
||||
WriteU32(0x50000042u);
|
||||
bytes.AddRange([0x11, 0, 0, 0]); // empty ModelData
|
||||
WriteU32(flags);
|
||||
WriteU32(0x00000748u); // Missile | ReportCollisions | AlignPath | PathClipped | Gravity
|
||||
|
||||
WriteU32(0x7F010123u); // full cell
|
||||
WriteF32(12.5f); WriteF32(25.0f); WriteF32(3.75f);
|
||||
WriteF32(1.0f); WriteF32(0.0f); WriteF32(0.0f); WriteF32(0.0f); // WXYZ
|
||||
WriteU32(0x09000001u);
|
||||
WriteU32(0x20000014u);
|
||||
WriteU32(0x34000005u);
|
||||
WriteU32(0x0200040Du);
|
||||
WriteU32(0x50000010u); WriteU32(3u);
|
||||
WriteU32(1u); WriteU32(0x50000011u); WriteU32(4u);
|
||||
WriteF32(1.25f);
|
||||
WriteF32(0.125f);
|
||||
WriteF32(0.05f);
|
||||
WriteF32(0.25f);
|
||||
WriteF32(10f); WriteF32(20f); WriteF32(30f);
|
||||
WriteF32(1f); WriteF32(2f); WriteF32(3f);
|
||||
WriteF32(0.1f); WriteF32(0.2f); WriteF32(0.3f);
|
||||
WriteU32(0xA5A5A5A5u);
|
||||
WriteU32(0x7FC01234u); // NaN intensity, preserving payload bits
|
||||
for (ushort i = 0; i < 9; i++) WriteU16((ushort)(0xFFF8u + i));
|
||||
Align4();
|
||||
|
||||
WriteU32(0u); // no optional WeenieHeader fields
|
||||
WriteString16L("Golden Projectile");
|
||||
WritePackedDword(7277u);
|
||||
WritePackedDword(0u);
|
||||
WriteU32(0x00000100u); // ammunition ItemType
|
||||
WriteU32(0u);
|
||||
Align4();
|
||||
return bytes.ToArray();
|
||||
|
||||
void WriteU16(ushort value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[2];
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buffer, value);
|
||||
bytes.AddRange(buffer.ToArray());
|
||||
}
|
||||
|
||||
void WriteU32(uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[4];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
bytes.AddRange(buffer.ToArray());
|
||||
}
|
||||
|
||||
void WriteF32(float value) => WriteU32(BitConverter.SingleToUInt32Bits(value));
|
||||
|
||||
void WritePackedDword(uint value)
|
||||
{
|
||||
if (value <= 0x7FFFu) WriteU16((ushort)value);
|
||||
else
|
||||
{
|
||||
WriteU16((ushort)(((value >> 16) & 0x7FFFu) | 0x8000u));
|
||||
WriteU16((ushort)value);
|
||||
}
|
||||
}
|
||||
|
||||
void WriteString16L(string value)
|
||||
{
|
||||
byte[] encoded = System.Text.Encoding.GetEncoding(1252).GetBytes(value);
|
||||
WriteU16(checked((ushort)encoded.Length));
|
||||
bytes.AddRange(encoded);
|
||||
Align4();
|
||||
}
|
||||
|
||||
void Align4()
|
||||
{
|
||||
while ((bytes.Count & 3) != 0) bytes.Add(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ACE SerializePhysicsData-shaped fixture with synthetic values. These
|
||||
/// small packets isolate mutually-exclusive and present-zero branches that
|
||||
/// a single broad CreateObject cannot cover.
|
||||
/// </summary>
|
||||
private static byte[] BuildBranchFixture(CreateObject.PhysicsDescriptionFlag flags, uint guid)
|
||||
{
|
||||
var bytes = new List<byte>();
|
||||
WriteU32(CreateObject.Opcode);
|
||||
WriteU32(guid);
|
||||
bytes.AddRange([0x11, 0, 0, 0]); // empty ModelData
|
||||
WriteU32((uint)flags);
|
||||
WriteU32(0); // explicit zero PhysicsState
|
||||
|
||||
if ((flags & CreateObject.PhysicsDescriptionFlag.Movement) != 0)
|
||||
WriteU32(0); // present Movement with an empty byte blob; no autonomous dword
|
||||
else if ((flags & CreateObject.PhysicsDescriptionFlag.AnimationFrame) != 0)
|
||||
WriteU32(0); // present placement/frame with value zero
|
||||
|
||||
if ((flags & CreateObject.PhysicsDescriptionFlag.PeTable) != 0)
|
||||
WriteU32(0); // present PhysicsScriptTable DID with value zero
|
||||
|
||||
for (ushort i = 0; i < 9; i++) WriteU16((ushort)(0x1100 + i));
|
||||
Align4();
|
||||
WriteU32(0); // no optional WeenieHeader fields
|
||||
WriteString16L("Physics Branch Fixture");
|
||||
WritePackedDword(7277);
|
||||
WritePackedDword(0);
|
||||
WriteU32(0x00000100u);
|
||||
WriteU32(0);
|
||||
Align4();
|
||||
return bytes.ToArray();
|
||||
|
||||
void WriteU16(ushort value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[2];
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buffer, value);
|
||||
bytes.AddRange(buffer.ToArray());
|
||||
}
|
||||
|
||||
void WriteU32(uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[4];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
bytes.AddRange(buffer.ToArray());
|
||||
}
|
||||
|
||||
void WritePackedDword(uint value)
|
||||
{
|
||||
if (value <= 0x7FFFu) WriteU16((ushort)value);
|
||||
else
|
||||
{
|
||||
WriteU16((ushort)(((value >> 16) & 0x7FFFu) | 0x8000u));
|
||||
WriteU16((ushort)value);
|
||||
}
|
||||
}
|
||||
|
||||
void WriteString16L(string value)
|
||||
{
|
||||
byte[] encoded = System.Text.Encoding.GetEncoding(1252).GetBytes(value);
|
||||
WriteU16(checked((ushort)encoded.Length));
|
||||
bytes.AddRange(encoded);
|
||||
Align4();
|
||||
}
|
||||
|
||||
void Align4()
|
||||
{
|
||||
while ((bytes.Count & 3) != 0) bytes.Add(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ProjectileVfxPacketFixtureTests
|
||||
{
|
||||
[Fact]
|
||||
public void DirectF754_PreservesExactTwelveByteWireShape()
|
||||
{
|
||||
ReadOnlySpan<byte> packet = ProjectileVfxPacketFixtures.DirectF754;
|
||||
|
||||
Assert.Equal(12, packet.Length);
|
||||
Assert.Equal(0xF754u, BinaryPrimitives.ReadUInt32LittleEndian(packet));
|
||||
Assert.Equal(0x50000042u, BinaryPrimitives.ReadUInt32LittleEndian(packet[4..]));
|
||||
Assert.Equal(0x33000099u, BinaryPrimitives.ReadUInt32LittleEndian(packet[8..]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypedF755_PreservesUnknownTypeAndNaNPayloadBits()
|
||||
{
|
||||
ReadOnlySpan<byte> packet = ProjectileVfxPacketFixtures.TypedF755UnknownNaN;
|
||||
|
||||
Assert.Equal(16, packet.Length);
|
||||
Assert.Equal(0xF755u, BinaryPrimitives.ReadUInt32LittleEndian(packet));
|
||||
Assert.Equal(0x50000042u, BinaryPrimitives.ReadUInt32LittleEndian(packet[4..]));
|
||||
Assert.Equal(0xA5A5A5A5u, BinaryPrimitives.ReadUInt32LittleEndian(packet[8..]));
|
||||
Assert.Equal(0x7FC01234u, BinaryPrimitives.ReadUInt32LittleEndian(packet[12..]));
|
||||
Assert.True(float.IsNaN(BinaryPrimitives.ReadSingleLittleEndian(packet[12..])));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GoldenCreateObject_ExercisesBroadProjectilePhysicsFieldsWithoutLosingAlignment()
|
||||
{
|
||||
byte[] packet = ProjectileVfxPacketFixtures.GoldenCreateObjectProjectilePhysicsFields;
|
||||
|
||||
var parsed = CreateObject.TryParse(packet);
|
||||
|
||||
Assert.NotNull(parsed);
|
||||
Assert.Equal(0x50000042u, parsed.Value.Guid);
|
||||
Assert.Equal("Golden Projectile", parsed.Value.Name);
|
||||
Assert.Equal(0x0200040Du, parsed.Value.SetupTableId);
|
||||
Assert.Equal(0x09000001u, parsed.Value.MotionTableId);
|
||||
Assert.Equal(1.25f, parsed.Value.ObjScale);
|
||||
Assert.Equal(0.125f, parsed.Value.Friction);
|
||||
Assert.Equal(0.05f, parsed.Value.Elasticity);
|
||||
Assert.Equal(0x50000010u, parsed.Value.ParentGuid);
|
||||
Assert.Equal((ushort)0xFFF8, parsed.Value.PositionSequence);
|
||||
Assert.Equal((ushort)0xFFF9, parsed.Value.MovementSequence);
|
||||
Assert.Equal((ushort)0x0000, parsed.Value.InstanceSequence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MutuallyExclusiveMovementAndAnimationFrameFixturesRemainAligned()
|
||||
{
|
||||
var movement = CreateObject.TryParse(ProjectileVfxPacketFixtures.CreateObjectMovementPresentEmpty);
|
||||
var animation = CreateObject.TryParse(ProjectileVfxPacketFixtures.CreateObjectAnimationFramePresentZero);
|
||||
|
||||
Assert.NotNull(movement);
|
||||
Assert.NotNull(animation);
|
||||
Assert.Equal(0x50000051u, movement.Value.Guid);
|
||||
Assert.Equal(0x50000052u, animation.Value.Guid);
|
||||
Assert.Null(movement.Value.PlacementId);
|
||||
Assert.Equal(0u, animation.Value.PlacementId);
|
||||
Assert.Equal((ushort)0x1108, movement.Value.InstanceSequence);
|
||||
Assert.Equal((ushort)0x1108, animation.Value.InstanceSequence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PeTableFixturesPinAbsentVersusPresentZeroWireShapes()
|
||||
{
|
||||
ReadOnlySpan<byte> present = ProjectileVfxPacketFixtures.CreateObjectPeTablePresentZero;
|
||||
ReadOnlySpan<byte> absent = ProjectileVfxPacketFixtures.CreateObjectPeTableAbsent;
|
||||
|
||||
Assert.Equal((uint)CreateObject.PhysicsDescriptionFlag.PeTable,
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(present[12..]));
|
||||
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(present[20..]));
|
||||
Assert.Equal(0u, BinaryPrimitives.ReadUInt32LittleEndian(absent[12..]));
|
||||
Assert.Equal(present.Length, absent.Length + sizeof(uint));
|
||||
Assert.NotNull(CreateObject.TryParse(present));
|
||||
Assert.NotNull(CreateObject.TryParse(absent));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue