diff --git a/src/AcDream.Core.Net/Messages/SetState.cs b/src/AcDream.Core.Net/Messages/SetState.cs
index a8a2126..70740a4 100644
--- a/src/AcDream.Core.Net/Messages/SetState.cs
+++ b/src/AcDream.Core.Net/Messages/SetState.cs
@@ -24,16 +24,18 @@ namespace AcDream.Core.Net.Messages;
///
///
///
-/// Total body size: 16 bytes from start (opcode + 12-byte payload).
+/// Total body size: 16 bytes (4-byte opcode + 12-byte payload).
///
///
///
/// Server-side reference:
/// references/ACE/Source/ACE.Server/Network/GameMessages/Messages/GameMessageSetState.cs:8-15
-/// (ACE writes the same field order but appears to use uint 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.
+/// — ACE writes the same field order using its UShortSequence.CurrentBytes
+/// helper (which calls BitConverter.GetBytes((ushort)value) = 2 bytes per
+/// sequence field), so its wire output matches holtburger's 12-byte payload.
+/// A one-shot ACDREAM_PROBE_BUILDING hex-dump in
+/// 's dispatcher (added in the same commit) emits
+/// the first SetState body bytes for runtime confirmation.
///
///
///
@@ -69,9 +71,9 @@ public static class SetState
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));
+ 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);
diff --git a/src/AcDream.Core.Net/WorldSession.cs b/src/AcDream.Core.Net/WorldSession.cs
index af7d695..85a571a 100644
--- a/src/AcDream.Core.Net/WorldSession.cs
+++ b/src/AcDream.Core.Net/WorldSession.cs
@@ -129,6 +129,19 @@ public sealed class WorldSession : IDisposable
///
public event Action? VectorUpdated;
+ ///
+ /// Fires when the server broadcasts a SetState (0xF74B) game
+ /// message — a previously-spawned entity's PhysicsState
+ /// bitmask changed post-CreateObject. Chiefly doors flipping
+ /// ETHEREAL_PS = 0x4 on Use (see ACE
+ /// WorldObjects/Door.cs:127, WorldObject.cs:640-660).
+ /// Subscribers route the new state into
+ /// so the
+ /// existing collision-exemption short-circuit honors the flip on the
+ /// next resolver tick.
+ ///
+ public event Action? StateUpdated;
+
///
/// Fires when the server sends a PlayerTeleport (0xF751) game message,
/// signalling that the player is entering portal space. The uint payload
@@ -375,6 +388,10 @@ public sealed class WorldSession : IDisposable
///
private bool _loginCompleteSent;
+ /// L.2g slice 1: one-shot guard so the [setstate-hex] probe
+ /// emits the first SetState's body bytes only, not 5–10/sec.
+ private bool _setStateHexDumped;
+
///
/// Phase B.2: per-session game-action sequence counter. Monotonically
/// incremented by and embedded in
@@ -750,6 +767,28 @@ public sealed class WorldSession : IDisposable
if (parsed is not null)
VectorUpdated?.Invoke(parsed.Value);
}
+ else if (op == SetState.Opcode)
+ {
+ // L.2g slice 1 (2026-05-12): server broadcasts SetState
+ // (0xF74B) when an entity's PhysicsState changes
+ // post-spawn — chiefly doors flipping ETHEREAL on Use.
+ // Holtburger validated wire format = 16 bytes (opcode +
+ // guid + state + 2×u16 sequence). One-shot probe-gated
+ // hex-dump (ACDREAM_PROBE_BUILDING) captures the wire
+ // bytes for confidence before declaring slice 1 done.
+ if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeBuildingEnabled
+ && !_setStateHexDumped)
+ {
+ _setStateHexDumped = true;
+ var hex = string.Join(" ", body.Take(Math.Min(body.Length, 32))
+ .Select(b => b.ToString("X2")));
+ Console.WriteLine($"[setstate-hex] body.len={body.Length} first-{Math.Min(body.Length, 32)}-bytes: {hex}");
+ }
+
+ var parsed = SetState.TryParse(body);
+ if (parsed is not null)
+ StateUpdated?.Invoke(parsed.Value);
+ }
else if (op == HearSpeech.LocalOpcode || op == HearSpeech.RangedOpcode)
{
// Phase H.1: local/ranged chat. Standalone GameMessage