From 536a608093551a40c45b507f5919a9ed2b21be7d Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 12 May 2026 22:28:04 +0200 Subject: [PATCH] feat(phys L.2g slice 1): WorldSession dispatches SetState (0xF74B) + hex probe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/AcDream.Core.Net/Messages/SetState.cs | 18 ++++++----- src/AcDream.Core.Net/WorldSession.cs | 39 +++++++++++++++++++++++ 2 files changed, 49 insertions(+), 8 deletions(-) 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