Three changes folded into one commit: 1. New public StateUpdated event on WorldSession + dispatcher branch for op == SetState.Opcode. Mirrors the VectorUpdated / MotionUpdated event pattern. GameWindow will subscribe in the next commit and feed the parsed (guid, newState) pair to ShadowObjectRegistry.UpdatePhysicsState. 2. One-shot probe-gated hex-dump (ACDREAM_PROBE_BUILDING) emits the first inbound SetState message's body bytes. Originally planned as a separate slice 1.5 confidence-check on holtburger's claimed 12-byte payload vs ACE's GameMessageSetState.cs. Folded into the dispatcher to avoid re-touching the same branch. The new _setStateHexDumped guard keeps the log clean — auto-close every 30s would otherwise produce noise. 3. Doc-comment polish on SetState.cs requested by Task 1's code review: remove false uncertainty about ACE's sequence-field width (ACE's UShortSequence.CurrentBytes provably writes 2 bytes via BitConverter), and align the 'total body size' phrasing with VectorUpdate.cs's convention. Folded here to avoid churning the file twice this slice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
86 lines
3.3 KiB
C#
86 lines
3.3 KiB
C#
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 (4-byte 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 using its <c>UShortSequence.CurrentBytes</c>
|
|
/// helper (which calls <c>BitConverter.GetBytes((ushort)value)</c> = 2 bytes per
|
|
/// sequence field), so its wire output matches holtburger's 12-byte payload.
|
|
/// A one-shot <c>ACDREAM_PROBE_BUILDING</c> hex-dump in
|
|
/// <see cref="WorldSession"/>'s dispatcher (added in the same commit) emits
|
|
/// the first SetState body bytes for runtime confirmation.
|
|
/// </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;
|
|
}
|
|
}
|
|
}
|