feat(phys L.2g slice 1): inbound SetState (0xF74B) parser
DTO + TryParse for the GameMessageSetState wire message. The server broadcasts this when an already-spawned entity's PhysicsState changes post-CreateObject — chiefly when a door's Ethereal bit toggles on Use. Wire format per holtburger SetStateData (validated against retail-format servers): u32 opcode + u32 guid + u32 state + u16 instanceSequence + u16 stateSequence = 16 bytes total. Mirrors the existing VectorUpdate.cs template. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
869677bc88
commit
2459f287e4
2 changed files with 127 additions and 0 deletions
84
src/AcDream.Core.Net/Messages/SetState.cs
Normal file
84
src/AcDream.Core.Net/Messages/SetState.cs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
using System;
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace AcDream.Core.Net.Messages;
|
||||
|
||||
/// <summary>
|
||||
/// Inbound <c>SetState</c> GameMessage (opcode <c>0xF74B</c>). The server
|
||||
/// broadcasts this whenever a previously-spawned entity's
|
||||
/// <c>PhysicsState</c> bitmask changes after <c>CreateObject</c> — chiefly
|
||||
/// when a door opens / closes (server flips <c>ETHEREAL_PS = 0x4</c>) or a
|
||||
/// spell projectile becomes ethereal post-impact.
|
||||
///
|
||||
/// <para>
|
||||
/// Wire layout (per
|
||||
/// <c>references/holtburger/crates/holtburger-protocol/src/messages/object/messages/properties.rs:117-122</c>,
|
||||
/// matched by every other acdream parser):
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item><b>u32 opcode</b> — 0xF74B</item>
|
||||
/// <item><b>u32 objectGuid</b></item>
|
||||
/// <item><b>u32 physicsState</b> — bitmask (acclient.h:2815 / 2819)</item>
|
||||
/// <item><b>u16 instanceSequence</b> — stale-packet rejection</item>
|
||||
/// <item><b>u16 stateSequence</b> — stale-packet rejection</item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para>
|
||||
/// Total body size: 16 bytes from start (opcode + 12-byte payload).
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Server-side reference:
|
||||
/// <c>references/ACE/Source/ACE.Server/Network/GameMessages/Messages/GameMessageSetState.cs:8-15</c>
|
||||
/// (ACE writes the same field order but appears to use <c>uint</c> for the
|
||||
/// sequence fields; verified against retail format by hex-dump probe in
|
||||
/// Task 5). Holtburger has been validated against a retail-format server,
|
||||
/// so its 12-byte payload is the trusted spec.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Named-retail anchor: <c>CPhysicsObj::set_state</c> at
|
||||
/// <c>docs/research/named-retail/acclient_2013_pseudo_c.txt:283044</c>
|
||||
/// describes the runtime state-store on the in-memory object
|
||||
/// (<c>this->state = arg2</c>). The wire format for this opcode is
|
||||
/// confirmed by holtburger's <c>SetStateData</c> struct; the named-retail
|
||||
/// decomp does not cover the deserialization path for this opcode.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class SetState
|
||||
{
|
||||
public const uint Opcode = 0xF74Bu;
|
||||
|
||||
public readonly record struct Parsed(
|
||||
uint Guid,
|
||||
uint PhysicsState,
|
||||
ushort InstanceSequence,
|
||||
ushort StateSequence);
|
||||
|
||||
/// <summary>
|
||||
/// Parse a 0xF74B body. <paramref name="body"/> must start with the
|
||||
/// 4-byte opcode (matches the convention used by VectorUpdate /
|
||||
/// UpdateMotion / UpdatePosition). Returns null on truncation or
|
||||
/// opcode mismatch.
|
||||
/// </summary>
|
||||
public static Parsed? TryParse(ReadOnlySpan<byte> body)
|
||||
{
|
||||
if (body.Length < 16) return null;
|
||||
try
|
||||
{
|
||||
uint opcode = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(0, 4));
|
||||
if (opcode != Opcode) return null;
|
||||
|
||||
uint guid = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(4, 4));
|
||||
uint state = BinaryPrimitives.ReadUInt32LittleEndian(body.Slice(8, 4));
|
||||
ushort instSeq = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(12, 2));
|
||||
ushort stateSeq = BinaryPrimitives.ReadUInt16LittleEndian(body.Slice(14, 2));
|
||||
|
||||
return new Parsed(guid, state, instSeq, stateSeq);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
43
tests/AcDream.Core.Net.Tests/Messages/SetStateTests.cs
Normal file
43
tests/AcDream.Core.Net.Tests/Messages/SetStateTests.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Net.Tests.Messages;
|
||||
|
||||
public class SetStateTests
|
||||
{
|
||||
[Fact]
|
||||
public void TryParse_WellFormedBody_ReturnsParsed()
|
||||
{
|
||||
var buf = new byte[16];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(0, 4), 0xF74Bu);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(4, 4), 0x000F4244u); // door guid
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(8, 4), 0x00000004u); // ETHEREAL bit
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(12, 2), (ushort)355);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buf.AsSpan(14, 2), (ushort)42);
|
||||
|
||||
var parsed = SetState.TryParse(buf);
|
||||
|
||||
Assert.NotNull(parsed);
|
||||
Assert.Equal(0x000F4244u, parsed.Value.Guid);
|
||||
Assert.Equal(0x00000004u, parsed.Value.PhysicsState);
|
||||
Assert.Equal((ushort)355, parsed.Value.InstanceSequence);
|
||||
Assert.Equal((ushort)42, parsed.Value.StateSequence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_Truncated_ReturnsNull()
|
||||
{
|
||||
var buf = new byte[10];
|
||||
Assert.Null(SetState.TryParse(buf));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_WrongOpcode_ReturnsNull()
|
||||
{
|
||||
var buf = new byte[16];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(0, 4), 0xF74Cu);
|
||||
Assert.Null(SetState.TryParse(buf));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue