docs(phys L.2g): slice 1 implementation plan
Step-by-step TDD plan for L.2g slice 1 — dynamic PhysicsState toggling.
Six commits' worth of work + a user-driven visual test at the Holtburg
inn doorway + a ship handoff commit.
Task 1: SetState (0xF74B) wire parser + xUnit tests (TDD).
Task 2: ShadowObjectRegistry.UpdatePhysicsState mutator + tests (TDD).
Task 3: WorldSession dispatcher branch + StateUpdated event.
Task 4: GameWindow subscribes, routes to registry, smoke-tests launch.
Task 5: One-shot hex-dump probe to settle holtburger 12-byte vs ACE
16-byte payload claim before declaring slice 1 done.
Task 6: Slice 0.5 freebie — extend [entity-source] log with state +
flags (the handoff's 'slice 1.6' suggestion).
Task 7: User-driven visual verification at Holtburg inn doorway.
Task 8: Ship handoff + CLAUDE.md / plan-of-record updates.
Risk surface: all changes are additive. No resolver edits. No broadphase
edits. No retail-port semantics changes. Per-task revert is safe.
Plan: docs/superpowers/plans/2026-05-12-phase-l2g-slice1.md
Spec: docs/superpowers/specs/2026-05-12-l2g-dynamic-physicsstate-design.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2c10dd4d67
commit
869677bc88
1 changed files with 899 additions and 0 deletions
899
docs/superpowers/plans/2026-05-12-phase-l2g-slice1.md
Normal file
899
docs/superpowers/plans/2026-05-12-phase-l2g-slice1.md
Normal file
|
|
@ -0,0 +1,899 @@
|
||||||
|
# Phase L.2g slice 1 — Dynamic PhysicsState Toggling Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Spec:** [docs/superpowers/specs/2026-05-12-l2g-dynamic-physicsstate-design.md](../specs/2026-05-12-l2g-dynamic-physicsstate-design.md) (committed in `2c10dd4`).
|
||||||
|
**Branch:** `claude/gallant-mestorf-3bf2e3` (do all commits here; user merges to main separately).
|
||||||
|
|
||||||
|
**Goal:** Parse inbound `GameMessageSetState (opcode 0xF74B)` and propagate
|
||||||
|
the new `PhysicsState` value into `ShadowObjectRegistry`'s cached per-entity
|
||||||
|
record so the existing `CollisionExemption.IsExempt(...)` short-circuit honors
|
||||||
|
runtime ETHEREAL flips — unblocking the M1 demo's *"open the inn door"* line.
|
||||||
|
|
||||||
|
**Architecture:** One new wire-message parser (`SetState`), one new event on
|
||||||
|
`WorldSession`, one new mutator method on `ShadowObjectRegistry`
|
||||||
|
(`UpdatePhysicsState`), one new subscriber in `GameWindow`. **No resolver
|
||||||
|
changes.** The existing `CollisionExemption.cs` short-circuit (cited at
|
||||||
|
`acclient_2013_pseudo_c.txt:276782`) already handles ETHEREAL; slice 1 just
|
||||||
|
feeds it fresh data.
|
||||||
|
|
||||||
|
**Tech Stack:** C# .NET 10, xUnit for tests, BinaryPrimitives for
|
||||||
|
little-endian reads. Mirror the existing `VectorUpdate.cs` parser pattern.
|
||||||
|
|
||||||
|
**Retail anchor (port reference):** `CPhysicsObj::set_state` at
|
||||||
|
`docs/research/named-retail/acclient_2013_pseudo_c.txt:283044`. The retail
|
||||||
|
implementation: `this->state = arg2` (line 283048) plus three side-effect
|
||||||
|
handlers for the changed-bit set (`0x800` lighting, `0x20` nodraw, `0x4000`
|
||||||
|
hidden). **Slice 1 ports only the state-store half**; ETHEREAL (`0x4`) is
|
||||||
|
not in the side-effect set, so the cosmetic handlers are not on the
|
||||||
|
M1-critical path and stay deferred.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
| File | Action | Responsibility |
|
||||||
|
|---|---|---|
|
||||||
|
| `src/AcDream.Core.Net/Messages/SetState.cs` | Create | New inbound DTO + `TryParse` for opcode `0xF74B`. Mirrors `VectorUpdate.cs`. |
|
||||||
|
| `src/AcDream.Core.Net/WorldSession.cs` | Modify | Add `StateUpdated` event + dispatch branch for `op == SetState.Opcode`. |
|
||||||
|
| `src/AcDream.Core/Physics/ShadowObjectRegistry.cs` | Modify | Add `UpdatePhysicsState(uint, uint)` method that mutates the cached `ShadowEntry.State` in every cell the entity occupies. |
|
||||||
|
| `src/AcDream.App/Rendering/GameWindow.cs` | Modify | Subscribe to `_liveSession.StateUpdated`, route `(guid, newState)` to `_physicsEngine.ShadowObjects.UpdatePhysicsState(...)`. Extend `[entity-source]` log with `state=` + `flags=` (slice 0.5 freebie). |
|
||||||
|
| `tests/AcDream.Core.Net.Tests/Messages/SetStateTests.cs` | Create | TryParse byte-level tests (well-formed, truncated, opcode-mismatch). |
|
||||||
|
| `tests/AcDream.Core.Tests/Physics/ShadowObjectRegistryTests.cs` | Modify | Add `UpdatePhysicsState_FlipsEthereal_NextLookupExempt` test using `CollisionExemption.ShouldSkip`. |
|
||||||
|
|
||||||
|
**No new project references needed** — all files live in existing assemblies.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Parser DTO + TryParse for `SetState` (opcode `0xF74B`)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/AcDream.Core.Net/Messages/SetState.cs`
|
||||||
|
- Create: `tests/AcDream.Core.Net.Tests/Messages/SetStateTests.cs`
|
||||||
|
|
||||||
|
**Reference template:** `src/AcDream.Core.Net/Messages/VectorUpdate.cs`
|
||||||
|
(read it before writing — same opcode dispatch convention, same body-length
|
||||||
|
check shape, same `BinaryPrimitives` style).
|
||||||
|
|
||||||
|
**Wire format** (per
|
||||||
|
`references/holtburger/crates/holtburger-protocol/src/messages/object/messages/properties.rs:117-122`,
|
||||||
|
matched by every other acdream parser):
|
||||||
|
|
||||||
|
```
|
||||||
|
offset 0 : u32 opcode (= 0xF74B)
|
||||||
|
offset 4 : u32 guid
|
||||||
|
offset 8 : u32 physics_state (bitmask; ETHEREAL = 0x4)
|
||||||
|
offset 12 : u16 instance_sequence
|
||||||
|
offset 14 : u16 state_sequence
|
||||||
|
Total: 16 bytes from start of body.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 1.1: Write the failing TryParse tests**
|
||||||
|
|
||||||
|
Create `tests/AcDream.Core.Net.Tests/Messages/SetStateTests.cs` with:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
// Build a synthetic SetState body: opcode + guid + state + 2×u16 seq.
|
||||||
|
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]; // < 16 bytes
|
||||||
|
Assert.Null(SetState.TryParse(buf));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryParse_WrongOpcode_ReturnsNull()
|
||||||
|
{
|
||||||
|
var buf = new byte[16];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buf.AsSpan(0, 4), 0xF74Cu); // UpdateMotion, not SetState
|
||||||
|
Assert.Null(SetState.TryParse(buf));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 1.2: Run tests to verify they fail (RED)**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/AcDream.Core.Net.Tests/AcDream.Core.Net.Tests.csproj --filter "FullyQualifiedName~SetStateTests"`
|
||||||
|
Expected: Compile error — `SetState` type not defined.
|
||||||
|
|
||||||
|
- [ ] **Step 1.3: Write the parser**
|
||||||
|
|
||||||
|
Create `src/AcDream.Core.Net/Messages/SetState.cs`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
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>
|
||||||
|
/// </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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 1.4: Run tests to verify they pass (GREEN)**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/AcDream.Core.Net.Tests/AcDream.Core.Net.Tests.csproj --filter "FullyQualifiedName~SetStateTests"`
|
||||||
|
Expected: 3 passed.
|
||||||
|
|
||||||
|
- [ ] **Step 1.5: Verify project build still green**
|
||||||
|
|
||||||
|
Run: `dotnet build`
|
||||||
|
Expected: Build succeeded, 0 errors, 0 new warnings.
|
||||||
|
|
||||||
|
- [ ] **Step 1.6: Commit**
|
||||||
|
|
||||||
|
```
|
||||||
|
git add src/AcDream.Core.Net/Messages/SetState.cs tests/AcDream.Core.Net.Tests/Messages/SetStateTests.cs
|
||||||
|
git commit -m "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>"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: `ShadowObjectRegistry.UpdatePhysicsState(guid, newState)`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/AcDream.Core/Physics/ShadowObjectRegistry.cs` (add new method after `UpdatePosition`, before `Deregister`)
|
||||||
|
- Modify: `tests/AcDream.Core.Tests/Physics/ShadowObjectRegistryTests.cs` (append new test)
|
||||||
|
|
||||||
|
**Design rationale:** `ShadowEntry` is a `readonly record struct` (value type)
|
||||||
|
stored as copies inside per-cell `List<ShadowEntry>`. Mutation pattern:
|
||||||
|
find every cell the entity occupies via `_entityToCells[entityId]`, then
|
||||||
|
replace each in-list copy with `list[i] = list[i] with { State = newState }`.
|
||||||
|
|
||||||
|
**Retail anchor:** `CPhysicsObj::set_state` at
|
||||||
|
`docs/research/named-retail/acclient_2013_pseudo_c.txt:283044`. Retail
|
||||||
|
does `this->state = arg2` (line 283048) — direct overwrite. Our cached
|
||||||
|
state lives in the registry copy, not the entity, so the equivalent is
|
||||||
|
"overwrite every shadow copy."
|
||||||
|
|
||||||
|
- [ ] **Step 2.1: Write the failing test**
|
||||||
|
|
||||||
|
Append to `tests/AcDream.Core.Tests/Physics/ShadowObjectRegistryTests.cs`
|
||||||
|
(top-of-file using directives already have `AcDream.Core.Physics` + xUnit):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// UpdatePhysicsState — L.2g slice 1 (doors flip ETHEREAL post-spawn)
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdatePhysicsState_FlipsEthereal_NextLookupSeesNewBits()
|
||||||
|
{
|
||||||
|
// Register a door-like entity with State=0 (closed = solid).
|
||||||
|
var reg = new ShadowObjectRegistry();
|
||||||
|
const uint doorId = 0x000F4244u;
|
||||||
|
reg.Register(doorId, 0x020019FFu, new Vector3(12f, 12f, 50f),
|
||||||
|
Quaternion.Identity, 1f, OffX, OffY, LbId,
|
||||||
|
state: 0u, flags: EntityCollisionFlags.None);
|
||||||
|
|
||||||
|
// Sanity: cached state starts at 0 (no ETHEREAL).
|
||||||
|
var before = reg.AllEntriesForDebug().Single(e => e.EntityId == doorId);
|
||||||
|
Assert.Equal(0u, before.State);
|
||||||
|
|
||||||
|
// Flip ETHEREAL_PS (0x4) — the server's "door is now open" message.
|
||||||
|
reg.UpdatePhysicsState(doorId, 0x00000004u);
|
||||||
|
|
||||||
|
// Cached state should now show the new bit.
|
||||||
|
var after = reg.AllEntriesForDebug().Single(e => e.EntityId == doorId);
|
||||||
|
Assert.Equal(0x00000004u, after.State);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdatePhysicsState_UnregisteredEntity_IsNoOp()
|
||||||
|
{
|
||||||
|
var reg = new ShadowObjectRegistry();
|
||||||
|
// No entity registered. Should not throw.
|
||||||
|
reg.UpdatePhysicsState(0xDEADBEEFu, 0x00000004u);
|
||||||
|
Assert.Equal(0, reg.TotalRegistered);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdatePhysicsState_EntitySpanningMultipleCells_AllCellsUpdated()
|
||||||
|
{
|
||||||
|
// Entity at (24,12) with radius=2 spans cells (0,0) and (1,0).
|
||||||
|
var reg = new ShadowObjectRegistry();
|
||||||
|
reg.Register(99u, 0x01000099u, new Vector3(24f, 12f, 50f),
|
||||||
|
Quaternion.Identity, 2f, OffX, OffY, LbId,
|
||||||
|
state: 0u);
|
||||||
|
|
||||||
|
reg.UpdatePhysicsState(99u, 0x00000004u);
|
||||||
|
|
||||||
|
uint cellA = LbId | 1u; // cx=0
|
||||||
|
uint cellB = LbId | (1u*8 + 0 + 1); // cx=1
|
||||||
|
var inA = reg.GetObjectsInCell(cellA).Single(e => e.EntityId == 99u);
|
||||||
|
var inB = reg.GetObjectsInCell(cellB).Single(e => e.EntityId == 99u);
|
||||||
|
Assert.Equal(0x00000004u, inA.State);
|
||||||
|
Assert.Equal(0x00000004u, inB.State);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You may need a `using System.Linq;` at the top of the test file. Add it if
|
||||||
|
not already present.
|
||||||
|
|
||||||
|
- [ ] **Step 2.2: Run tests to verify they fail (RED)**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~UpdatePhysicsState"`
|
||||||
|
Expected: Compile error — `UpdatePhysicsState` method not defined.
|
||||||
|
|
||||||
|
- [ ] **Step 2.3: Implement `UpdatePhysicsState`**
|
||||||
|
|
||||||
|
Insert into `src/AcDream.Core/Physics/ShadowObjectRegistry.cs` after the
|
||||||
|
`UpdatePosition` method (around line 127, before the `Deregister` summary
|
||||||
|
comment):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>
|
||||||
|
/// Update the cached <see cref="ShadowEntry.State"/> bits for an
|
||||||
|
/// already-registered entity. Called by the inbound
|
||||||
|
/// <c>SetState (0xF74B)</c> dispatcher when the server broadcasts a
|
||||||
|
/// post-spawn <c>PhysicsState</c> change — chiefly doors flipping
|
||||||
|
/// <c>ETHEREAL_PS = 0x4</c> on Use, so the
|
||||||
|
/// <see cref="CollisionExemption.ShouldSkip"/> short-circuit can honor
|
||||||
|
/// the new state on the next resolve.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Retail equivalent: <c>CPhysicsObj::set_state</c> at
|
||||||
|
/// <c>docs/research/named-retail/acclient_2013_pseudo_c.txt:283044</c>
|
||||||
|
/// — direct write `this->state = arg2`. Retail also fires side-effect
|
||||||
|
/// handlers for the 0x800 (lighting), 0x20 (nodraw), 0x4000 (hidden)
|
||||||
|
/// changed bits; ETHEREAL (0x4) doesn't trigger any of them, so slice 1
|
||||||
|
/// scopes to the bare state-write.
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Implementation: <see cref="ShadowEntry"/> is a value-type record
|
||||||
|
/// copied into per-cell lists, so we rewrite the copy in each cell the
|
||||||
|
/// entity occupies. Unregistered entities are a no-op (callers don't
|
||||||
|
/// have to gate).
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public void UpdatePhysicsState(uint entityId, uint newState)
|
||||||
|
{
|
||||||
|
if (!_entityToCells.TryGetValue(entityId, out var cellIds))
|
||||||
|
return; // not registered — no-op
|
||||||
|
|
||||||
|
foreach (var cellId in cellIds)
|
||||||
|
{
|
||||||
|
if (!_cells.TryGetValue(cellId, out var list)) continue;
|
||||||
|
for (int i = 0; i < list.Count; i++)
|
||||||
|
{
|
||||||
|
if (list[i].EntityId == entityId)
|
||||||
|
list[i] = list[i] with { State = newState };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2.4: Run tests to verify they pass (GREEN)**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/AcDream.Core.Tests/AcDream.Core.Tests.csproj --filter "FullyQualifiedName~UpdatePhysicsState"`
|
||||||
|
Expected: 3 passed.
|
||||||
|
|
||||||
|
- [ ] **Step 2.5: Verify full test suite still green (no regressions)**
|
||||||
|
|
||||||
|
Run: `dotnet test`
|
||||||
|
Expected: All previously-passing tests still pass. **Note:** ~8 pre-existing
|
||||||
|
failures may be in the baseline (see `docs/research/2026-05-13-l2d-slice1-shipped-handoff.md`
|
||||||
|
"Open concerns"); ensure the count does not increase. Stash + rerun to
|
||||||
|
confirm if uncertain: `git stash && dotnet test 2>&1 | findstr Failed` then
|
||||||
|
`git stash pop`.
|
||||||
|
|
||||||
|
- [ ] **Step 2.6: Commit**
|
||||||
|
|
||||||
|
```
|
||||||
|
git add src/AcDream.Core/Physics/ShadowObjectRegistry.cs tests/AcDream.Core.Tests/Physics/ShadowObjectRegistryTests.cs
|
||||||
|
git commit -m "feat(phys L.2g slice 1): ShadowObjectRegistry.UpdatePhysicsState
|
||||||
|
|
||||||
|
New mutator that overwrites cached PhysicsState bits on every shadow copy
|
||||||
|
of the named entity. The existing CollisionExemption.ShouldSkip(...) check
|
||||||
|
(acclient_2013_pseudo_c.txt:276782) reads the same cached field, so a
|
||||||
|
post-spawn ETHEREAL flip is now honored on the next resolver tick without
|
||||||
|
any resolver-path change.
|
||||||
|
|
||||||
|
Retail anchor: CPhysicsObj::set_state at acclient_2013_pseudo_c.txt:283044.
|
||||||
|
Slice 1 scopes to the bare state-write — retail's cosmetic side-effect
|
||||||
|
handlers (0x800 lighting, 0x20 nodraw, 0x4000 hidden) don't fire for the
|
||||||
|
ETHEREAL bit and stay deferred.
|
||||||
|
|
||||||
|
Three TDD tests cover: ETHEREAL flip from 0->0x4; unregistered-entity
|
||||||
|
no-op; entity spanning multiple cells gets all copies updated.
|
||||||
|
|
||||||
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: Wire `0xF74B` into `WorldSession` dispatcher + new event
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/AcDream.Core.Net/WorldSession.cs` (one new event declaration near the existing `VectorUpdated`/`MotionUpdated` events; one new `else if` branch in the inbound dispatcher near `op == VectorUpdate.Opcode`)
|
||||||
|
|
||||||
|
**Reference pattern:** Read the existing `VectorUpdate.Opcode` branch first
|
||||||
|
(it's at WorldSession.cs:739–752). Copy its shape exactly.
|
||||||
|
|
||||||
|
- [ ] **Step 3.1: Add the public event declaration**
|
||||||
|
|
||||||
|
Find the existing `public event Action<...>? VectorUpdated;` declaration
|
||||||
|
in `WorldSession.cs` (near line 119, in the events region). Add a sibling:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>
|
||||||
|
/// Fires when the server broadcasts a <c>SetState (0xF74B)</c> game
|
||||||
|
/// message — a previously-spawned entity's <c>PhysicsState</c>
|
||||||
|
/// bitmask changed post-CreateObject. Chiefly doors flipping
|
||||||
|
/// <c>ETHEREAL_PS = 0x4</c> on Use (see ACE
|
||||||
|
/// <c>WorldObjects/Door.cs:127</c>, <c>WorldObject.cs:640-660</c>).
|
||||||
|
/// Subscribers route the new state into
|
||||||
|
/// <see cref="ShadowObjectRegistry.UpdatePhysicsState"/> so the
|
||||||
|
/// existing collision-exemption short-circuit honors the flip on the
|
||||||
|
/// next resolver tick.
|
||||||
|
/// </summary>
|
||||||
|
public event Action<SetState.Parsed>? StateUpdated;
|
||||||
|
```
|
||||||
|
|
||||||
|
Place it immediately after the existing `VectorUpdated` event for grep-
|
||||||
|
findability.
|
||||||
|
|
||||||
|
- [ ] **Step 3.2: Add the dispatcher branch**
|
||||||
|
|
||||||
|
In the inbound game-message dispatcher (the chain of `else if (op == X.Opcode)`
|
||||||
|
branches in the same file), add this branch immediately after the
|
||||||
|
`VectorUpdate.Opcode` branch:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
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). ACE
|
||||||
|
// GameMessageSetState.cs writes the same field order
|
||||||
|
// but appears to use u32 for the sequences; Task 5's
|
||||||
|
// hex-dump probe settles the actual byte count.
|
||||||
|
var parsed = SetState.TryParse(body);
|
||||||
|
if (parsed is not null)
|
||||||
|
StateUpdated?.Invoke(parsed.Value);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `using AcDream.Core.Net.Messages;` directive should already be at the
|
||||||
|
top of WorldSession.cs (it's used by every existing parser). Confirm,
|
||||||
|
don't add a duplicate.
|
||||||
|
|
||||||
|
- [ ] **Step 3.3: Verify build still green**
|
||||||
|
|
||||||
|
Run: `dotnet build`
|
||||||
|
Expected: Build succeeded, 0 errors.
|
||||||
|
|
||||||
|
- [ ] **Step 3.4: Commit**
|
||||||
|
|
||||||
|
```
|
||||||
|
git add src/AcDream.Core.Net/WorldSession.cs
|
||||||
|
git commit -m "feat(phys L.2g slice 1): WorldSession dispatches SetState (0xF74B)
|
||||||
|
|
||||||
|
New StateUpdated event + dispatcher branch routes inbound SetState
|
||||||
|
messages to subscribers. Mirrors the existing VectorUpdated /
|
||||||
|
MotionUpdated event pattern. GameWindow will subscribe in the next
|
||||||
|
commit and feed the parsed (guid, newState) pair to
|
||||||
|
ShadowObjectRegistry.UpdatePhysicsState.
|
||||||
|
|
||||||
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: Subscribe in `GameWindow` and feed `ShadowObjectRegistry`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (one new subscription line + one new handler method)
|
||||||
|
|
||||||
|
This is the final wiring step. After this commit, the server's "door opened"
|
||||||
|
SetState is end-to-end honored by the collision system.
|
||||||
|
|
||||||
|
- [ ] **Step 4.1: Add the subscription**
|
||||||
|
|
||||||
|
Find the block in `GameWindow.cs` where `_liveSession.MotionUpdated +=
|
||||||
|
OnLiveMotionUpdated;` and `_liveSession.PositionUpdated +=
|
||||||
|
OnLivePositionUpdated;` are wired (around line 1791). Add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
_liveSession.StateUpdated += OnLiveStateUpdated;
|
||||||
|
```
|
||||||
|
|
||||||
|
Place it after `_liveSession.VectorUpdated += OnLiveVectorUpdated;` so the
|
||||||
|
event-subscription order is co-located with its peers.
|
||||||
|
|
||||||
|
- [ ] **Step 4.2: Add the handler method**
|
||||||
|
|
||||||
|
Find the existing `OnLiveVectorUpdated` method body in the same file
|
||||||
|
(grep `private void OnLiveVectorUpdated`). Add a sibling handler
|
||||||
|
immediately after it:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>
|
||||||
|
/// L.2g slice 1: inbound SetState (0xF74B) handler. Propagates the
|
||||||
|
/// new <c>PhysicsState</c> bits into ShadowObjectRegistry so the
|
||||||
|
/// existing <see cref="CollisionExemption.ShouldSkip"/> check honors
|
||||||
|
/// the flip on the next resolver tick. Chiefly doors:
|
||||||
|
/// server flips <c>ETHEREAL_PS = 0x4</c> on Use, the door's
|
||||||
|
/// cylinder collision stops blocking the threshold.
|
||||||
|
/// </summary>
|
||||||
|
private void OnLiveStateUpdated(AcDream.Core.Net.Messages.SetState.Parsed parsed)
|
||||||
|
{
|
||||||
|
_physicsEngine.ShadowObjects.UpdatePhysicsState(parsed.Guid, parsed.PhysicsState);
|
||||||
|
|
||||||
|
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeBuildingEnabled)
|
||||||
|
Console.WriteLine(System.FormattableString.Invariant(
|
||||||
|
$"[setstate] guid=0x{parsed.Guid:X8} state=0x{parsed.PhysicsState:X8} instSeq={parsed.InstanceSequence} stateSeq={parsed.StateSequence}"));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4.3: Verify build still green**
|
||||||
|
|
||||||
|
Run: `dotnet build`
|
||||||
|
Expected: Build succeeded, 0 errors.
|
||||||
|
|
||||||
|
- [ ] **Step 4.4: Smoke-test that no regression breaks the launch path**
|
||||||
|
|
||||||
|
Run a quick non-interactive smoke (do NOT do the full visual test yet —
|
||||||
|
that's Task 7):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
|
||||||
|
$env:ACDREAM_LIVE = "0" # offline; just verify the binary starts
|
||||||
|
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 |
|
||||||
|
Select-String -Pattern "Exception|FATAL" |
|
||||||
|
Select-Object -First 5
|
||||||
|
```
|
||||||
|
|
||||||
|
Then kill the process. Expected: no startup exception, no FATAL. If
|
||||||
|
anything blows up, the new handler subscription or the registry mutator
|
||||||
|
broke something in the live-session attach path.
|
||||||
|
|
||||||
|
- [ ] **Step 4.5: Commit**
|
||||||
|
|
||||||
|
```
|
||||||
|
git add src/AcDream.App/Rendering/GameWindow.cs
|
||||||
|
git commit -m "feat(phys L.2g slice 1): GameWindow routes SetState into registry
|
||||||
|
|
||||||
|
End-to-end wiring: WorldSession.StateUpdated fires -> GameWindow
|
||||||
|
OnLiveStateUpdated -> ShadowObjectRegistry.UpdatePhysicsState -> next
|
||||||
|
resolver tick sees the updated ETHEREAL bit and CollisionExemption
|
||||||
|
short-circuits the door cylinder. After this commit the M1 'open the
|
||||||
|
inn door' scenario is unblocked at the code-path level; visual
|
||||||
|
verification follows in slice 1's manual test (Task 7).
|
||||||
|
|
||||||
|
The handler also emits a [setstate] diagnostic line when
|
||||||
|
ACDREAM_PROBE_BUILDING is enabled — gives a greppable trail when the
|
||||||
|
visual test runs.
|
||||||
|
|
||||||
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: Hex-dump probe for first SetState payload (wire-byte verification)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/AcDream.Core.Net/WorldSession.cs` (extend the existing `else if (op == SetState.Opcode)` branch added in Task 3)
|
||||||
|
|
||||||
|
**Why:** ACE's `GameMessageSetState.cs:13-14` writes `Sequences.GetCurrentSequence(...)`
|
||||||
|
+ `Sequences.GetNextSequence(...)` as `uint` calls — potentially 4 bytes
|
||||||
|
each (16-byte total payload) instead of holtburger's 12 bytes. We default to
|
||||||
|
holtburger's spec because it's been validated against live retail-format
|
||||||
|
servers, but we want one-shot evidence on real wire bytes before declaring
|
||||||
|
slice 1 done.
|
||||||
|
|
||||||
|
The probe is gated on `ACDREAM_PROBE_BUILDING` (existing env var from
|
||||||
|
L.2d slice 1) and fires once per SetState message; the body bytes are
|
||||||
|
short enough that this is cheap.
|
||||||
|
|
||||||
|
- [ ] **Step 5.1: Extend the dispatcher branch with a hex-dump**
|
||||||
|
|
||||||
|
Update the `else if (op == SetState.Opcode)` branch from Task 3 to:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
else if (op == SetState.Opcode)
|
||||||
|
{
|
||||||
|
// L.2g slice 1 (2026-05-12) — see Task 3 above for the
|
||||||
|
// event-routing intent. The probe-gated hex-dump here
|
||||||
|
// captures the wire bytes one-shot per session so we can
|
||||||
|
// confirm holtburger's 12-byte payload format (vs ACE's
|
||||||
|
// GameMessageSetState.cs claim of u32 sequences = 16
|
||||||
|
// bytes) 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);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the one-shot flag field near the top of the `WorldSession` class
|
||||||
|
(group with other `_dump*Enabled` flags — grep `private bool _` to find
|
||||||
|
the cluster):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
/// <summary>L.2g slice 1: one-shot guard so the [setstate-hex] probe
|
||||||
|
/// emits the first SetState's body bytes only, not 5–10/sec.</summary>
|
||||||
|
private bool _setStateHexDumped;
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: the `body.Take(...)` requires `using System.Linq;` — already present.
|
||||||
|
|
||||||
|
- [ ] **Step 5.2: Verify build still green**
|
||||||
|
|
||||||
|
Run: `dotnet build`
|
||||||
|
Expected: Build succeeded, 0 errors.
|
||||||
|
|
||||||
|
- [ ] **Step 5.3: Commit**
|
||||||
|
|
||||||
|
```
|
||||||
|
git add src/AcDream.Core.Net/WorldSession.cs
|
||||||
|
git commit -m "feat(phys L.2g slice 1): one-shot hex-dump probe for SetState payload
|
||||||
|
|
||||||
|
Probe-gated diagnostic (ACDREAM_PROBE_BUILDING) emits the first inbound
|
||||||
|
SetState message's body bytes so we can confirm holtburger's 12-byte
|
||||||
|
payload format vs ACE's GameMessageSetState.cs claim of u32 sequences
|
||||||
|
(16-byte payload). One-shot via _setStateHexDumped — won't flood the
|
||||||
|
log when doors auto-close every 30s.
|
||||||
|
|
||||||
|
If the hex-dump shows body.len > 16, the parser's body-length gate at
|
||||||
|
SetState.cs needs widening (and the seq-field reads shifted accordingly).
|
||||||
|
If it shows 16, we ship as-is.
|
||||||
|
|
||||||
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: Slice 0.5 — extend `[entity-source]` log with `state` + `flags`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/AcDream.App/Rendering/GameWindow.cs` (extend the existing `[entity-source]` log line — there are 2 sites; modify both for consistency)
|
||||||
|
|
||||||
|
**Why:** The L.2d slice 1 handoff flagged this as a useful "slice 1.6"
|
||||||
|
addendum. It's 5 LOC, fold-into-slice-1-freebie. Makes ETHEREAL flips
|
||||||
|
greppable end-to-end: spawn -> registry update -> resolver effect.
|
||||||
|
|
||||||
|
- [ ] **Step 6.1: Find both `[entity-source]` log sites**
|
||||||
|
|
||||||
|
Grep `[entity-source]` in `src/AcDream.App/Rendering/GameWindow.cs` and
|
||||||
|
note the two `Console.WriteLine` calls (one is around line 2978 from the
|
||||||
|
RegisterLiveEntityForCollision path; the other should be in the
|
||||||
|
landblock-baked static registration path — grep confirms by file). Both
|
||||||
|
need the same suffix addition.
|
||||||
|
|
||||||
|
- [ ] **Step 6.2: Extend both log lines**
|
||||||
|
|
||||||
|
For each `[entity-source]` line, append `state=0x{state:X8} flags={flags}`
|
||||||
|
to the format string. Example transformation:
|
||||||
|
|
||||||
|
Before:
|
||||||
|
```csharp
|
||||||
|
Console.WriteLine(System.FormattableString.Invariant(
|
||||||
|
$"[entity-source] id=0x{entity.Id:X8} entityId=0x{entity.Id:X8} src=0x{entity.SourceGfxObjOrSetupId:X8} gfxObj=0x{entity.SourceGfxObjOrSetupId:X8} lb=0x{spawn.Position.Value.LandblockId:X8} type=Cylinder note=server-spawn-root"));
|
||||||
|
```
|
||||||
|
|
||||||
|
After (note: the local variables `state` and `flags` should already be
|
||||||
|
in scope at both sites — they're computed just before the
|
||||||
|
`ShadowObjects.Register(...)` call; grep upward 5–10 lines from each log
|
||||||
|
site to confirm):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
Console.WriteLine(System.FormattableString.Invariant(
|
||||||
|
$"[entity-source] id=0x{entity.Id:X8} entityId=0x{entity.Id:X8} src=0x{entity.SourceGfxObjOrSetupId:X8} gfxObj=0x{entity.SourceGfxObjOrSetupId:X8} lb=0x{spawn.Position.Value.LandblockId:X8} type=Cylinder note=server-spawn-root state=0x{state:X8} flags={flags}"));
|
||||||
|
```
|
||||||
|
|
||||||
|
If the `state` or `flags` variables are scoped differently at one site
|
||||||
|
(e.g. one site is for landblock-baked statics that always have state=0),
|
||||||
|
substitute the literal `0u` or `EntityCollisionFlags.None` and add a
|
||||||
|
comment noting the static-default. Keep the field names identical at both
|
||||||
|
sites so a single regex `state=0x([0-9A-F]+)` catches every entry.
|
||||||
|
|
||||||
|
- [ ] **Step 6.3: Verify build still green**
|
||||||
|
|
||||||
|
Run: `dotnet build`
|
||||||
|
Expected: Build succeeded, 0 errors.
|
||||||
|
|
||||||
|
- [ ] **Step 6.4: Commit**
|
||||||
|
|
||||||
|
```
|
||||||
|
git add src/AcDream.App/Rendering/GameWindow.cs
|
||||||
|
git commit -m "feat(phys L.2g slice 1): extend [entity-source] log with state + flags
|
||||||
|
|
||||||
|
5-LOC freebie folded into L.2g slice 1: the [entity-source] probe now
|
||||||
|
emits the PhysicsState bits + EntityCollisionFlags decoded at
|
||||||
|
registration. Combined with the new [setstate] handler log line, this
|
||||||
|
makes door open/close events fully greppable end-to-end:
|
||||||
|
spawn -> [entity-source] guid=... state=0x00000000 ...
|
||||||
|
Use -> [setstate] guid=... state=0x00000004 ...
|
||||||
|
close -> [setstate] guid=... state=0x00000000 ...
|
||||||
|
|
||||||
|
Resolves the 'slice 1.6' suggestion from
|
||||||
|
docs/research/2026-05-13-l2d-slice1-shipped-handoff.md.
|
||||||
|
|
||||||
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 7: Visual verification at Holtburg inn doorway
|
||||||
|
|
||||||
|
**Files:** None — this is a user-driven test. Document the recipe; report
|
||||||
|
results in the handoff doc (Task 8).
|
||||||
|
|
||||||
|
**Acceptance:**
|
||||||
|
1. Walk acdream `+Acdream` into the Holtburg inn doorway. **Expected: blocked at threshold.**
|
||||||
|
2. Click the door (Use action). **Expected: door swings open; `[setstate]` log line emits with `state=0x00000004`; walk through clears.**
|
||||||
|
3. Wait ~30 seconds. **Expected: door auto-closes; `[setstate]` log line emits with `state=0x00000000`; threshold blocks again.**
|
||||||
|
4. Inspect the `[setstate-hex]` line emitted on the first SetState — confirm `body.len=16`. If it's 20 instead, slice 1 has a bug to file as 1b.
|
||||||
|
|
||||||
|
- [ ] **Step 7.1: Launch the client with probes enabled**
|
||||||
|
|
||||||
|
Wait ~5 seconds since the last close (per CLAUDE.md's logout-before-reconnect
|
||||||
|
note) then:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
|
||||||
|
$env:ACDREAM_LIVE = "1"
|
||||||
|
$env:ACDREAM_TEST_HOST = "127.0.0.1"
|
||||||
|
$env:ACDREAM_TEST_PORT = "9000"
|
||||||
|
$env:ACDREAM_TEST_USER = "testaccount"
|
||||||
|
$env:ACDREAM_TEST_PASS = "testpassword"
|
||||||
|
$env:ACDREAM_DEVTOOLS = "1"
|
||||||
|
$env:ACDREAM_PROBE_BUILDING = "1"
|
||||||
|
$env:ACDREAM_PROBE_RESOLVE = "1"
|
||||||
|
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 |
|
||||||
|
Tee-Object -FilePath "launch-l2g-slice1.log"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7.2: Manually perform the four-step scenario**
|
||||||
|
|
||||||
|
(User-driven. See Acceptance list above.)
|
||||||
|
|
||||||
|
- [ ] **Step 7.3: Inspect the log for the four expected lines**
|
||||||
|
|
||||||
|
After closing the client window:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Select-String -Path launch-l2g-slice1.log -Pattern "setstate-hex|setstate.*guid|entity-source.*Door"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected matches:
|
||||||
|
- One `[setstate-hex] body.len=16 first-16-bytes: 4B F7 ...` line.
|
||||||
|
- One `[entity-source] ... name=Door ... state=0x00000000 ...` (or similar).
|
||||||
|
- A `[setstate] guid=0x000F.... state=0x00000004 ...` after the Use click.
|
||||||
|
- A `[setstate] guid=0x000F.... state=0x00000000 ...` ~30s after the previous.
|
||||||
|
|
||||||
|
- [ ] **Step 7.4: Decide ship-or-fix**
|
||||||
|
|
||||||
|
Three outcomes:
|
||||||
|
- **All four log lines match + door scenario works visually:** slice 1 ships. Proceed to Task 8.
|
||||||
|
- **Log lines correct but visual scenario fails (door visually opens but player still blocked):** the resolver is reading stale state from somewhere we haven't found. Stop and file a "slice 1b — find the second cache layer" note.
|
||||||
|
- **`[setstate-hex] body.len=20`:** ACE's u32 sequence claim is real. Widen `SetState.cs` body-length gate (`16` -> `20`) and shift sequence reads to `body.Slice(12, 4)` + `body.Slice(16, 4)` (read as `uint`, cast to `ushort` if values are small — high bits will be zero per ACE's `Sequences` design). Re-run from Task 7.1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 8: Ship handoff doc + roadmap update
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `docs/research/2026-05-XX-l2g-slice1-shipped-handoff.md` (replace `XX` with the actual ship date)
|
||||||
|
- Modify: `CLAUDE.md` (replace the "the natural next step is the L.2g slice 1 implementation" paragraph with a "Phase L.2g slice 1 shipped <date>" paragraph mirroring the L.2a paragraph style)
|
||||||
|
- Modify: `docs/plans/2026-04-29-movement-collision-conformance.md` (under the L.2g section, add a "Current shipped slice" subsection noting slice 1 + its commit hashes)
|
||||||
|
|
||||||
|
- [ ] **Step 8.1: Write the ship handoff doc**
|
||||||
|
|
||||||
|
Use the existing handoff at
|
||||||
|
`docs/research/2026-05-13-l2d-slice1-shipped-handoff.md` as a template. The
|
||||||
|
new doc should cover:
|
||||||
|
|
||||||
|
- TL;DR: what landed, did the visual test pass.
|
||||||
|
- What shipped (commit hash + subject per commit from Tasks 1–6).
|
||||||
|
- What the visual test showed (the four log-line samples from Task 7.3).
|
||||||
|
- Wire-byte width resolution (12-byte vs 16-byte — whichever the hex-dump
|
||||||
|
showed).
|
||||||
|
- Side findings (anything noticed during visual test — door animation
|
||||||
|
flickers, audio not playing, etc — file under "deferred").
|
||||||
|
- Next-session candidates (L.2g slice 2 animation confirmation, deferred UX
|
||||||
|
polish, OR pick from CLAUDE.md's now-revised "Next phase candidates"
|
||||||
|
list).
|
||||||
|
|
||||||
|
- [ ] **Step 8.2: Update CLAUDE.md**
|
||||||
|
|
||||||
|
Find the "Currently in Phase L.2 (Movement & Collision Conformance)"
|
||||||
|
paragraph. Replace its "the natural next step is the L.2g slice 1
|
||||||
|
implementation" sentence with "L.2g slice 1 shipped <date> — doors honor
|
||||||
|
ETHEREAL flips end-to-end; visual-verified at Holtburg inn doorway." Add
|
||||||
|
a "**Phase L.2g slice 1 shipped <date>.**" descriptive paragraph after
|
||||||
|
the L.2a paragraph (mirror the L.2a paragraph's depth).
|
||||||
|
|
||||||
|
In the "**Next phase candidates**" list, demote the current L.2g item
|
||||||
|
out and pick whichever is the next sensible candidate (likely L.2g slice 2
|
||||||
|
animation confirmation OR a non-L.2 visual-fidelity item — depends on what
|
||||||
|
the visual test in Task 7 showed).
|
||||||
|
|
||||||
|
- [ ] **Step 8.3: Update the L.2 plan-of-record**
|
||||||
|
|
||||||
|
In `docs/plans/2026-04-29-movement-collision-conformance.md`, under the
|
||||||
|
L.2g section, add a "Current shipped slice (<date>):" subsection
|
||||||
|
listing the slice 1 commit hashes + their subjects (use git log to fill
|
||||||
|
in). Mirror the L.2c "Current shipped slice (2026-04-30):" subsection
|
||||||
|
style.
|
||||||
|
|
||||||
|
- [ ] **Step 8.4: Commit**
|
||||||
|
|
||||||
|
```
|
||||||
|
git add CLAUDE.md docs/plans/2026-04-29-movement-collision-conformance.md docs/research/2026-05-XX-l2g-slice1-shipped-handoff.md
|
||||||
|
git commit -m "docs(phys L.2g): slice 1 shipped handoff + plan-of-record + CLAUDE.md
|
||||||
|
|
||||||
|
Slice 1 visual-verified at Holtburg inn doorway: walking into closed door
|
||||||
|
is blocked, Use opens it, walk-through clears, auto-close re-blocks at 30s.
|
||||||
|
Wire-byte width settled (see handoff doc).
|
||||||
|
|
||||||
|
L.2g slice 2 (animation confirmation) becomes the next candidate IF the
|
||||||
|
visual test showed door animation not playing; otherwise slice 2 is a
|
||||||
|
verify-only no-op and we move to the next phase candidate.
|
||||||
|
|
||||||
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Plan self-review
|
||||||
|
|
||||||
|
**1. Spec coverage check:**
|
||||||
|
|
||||||
|
| Spec section | Task |
|
||||||
|
|---|---|
|
||||||
|
| Slice 1 — parse SetState (0xF74B) | Tasks 1 + 3 + 5 |
|
||||||
|
| Slice 1 — plumb new state into ShadowObjectRegistry | Tasks 2 + 4 |
|
||||||
|
| Slice 1 — visual verification at Holtburg | Task 7 |
|
||||||
|
| Slice 0.5 — extend [entity-source] log with state + flags | Task 6 |
|
||||||
|
| Open Q1 — wire-byte width | Task 5 (hex-dump probe) + Task 7.4 (decision branch) |
|
||||||
|
| Open Q2 — UpdateMotion drives non-creature entities (door swing animation) | **Deferred to slice 2** (per the spec — animation is verify-only) |
|
||||||
|
| Open Q3 — SetState delivered to the player who triggered Use | Task 7 visual test verifies (covered implicitly by the four-step scenario) |
|
||||||
|
| Acceptance — design spec, plan-of-record, milestones, CLAUDE.md all reference L.2g | Already done in `2c10dd4`; Task 8 closes the loop with the slice 1 ship handoff |
|
||||||
|
| Named retail citation in slice 1 code | Task 2.3 cites `acclient_2013_pseudo_c.txt:283044`; Task 1.3 cites the holtburger struct |
|
||||||
|
|
||||||
|
**2. Placeholder scan:** No `TBD`, `TODO`, "fill in later." `<date>` in
|
||||||
|
Task 8 is a deliberate placeholder for the engineer to fill in at ship
|
||||||
|
time — flagged as such in the handoff doc template, not a plan-writing
|
||||||
|
oversight. The `Task 8.1` doc filename uses `2026-05-XX` for the same
|
||||||
|
reason.
|
||||||
|
|
||||||
|
**3. Type consistency:** `SetState.Parsed(Guid, PhysicsState, InstanceSequence, StateSequence)`
|
||||||
|
used consistently in Tasks 1, 3, 4, 5. `UpdatePhysicsState(uint entityId, uint newState)`
|
||||||
|
signature consistent in Tasks 2 + 4. `ShadowEntry.State` matches the
|
||||||
|
existing struct definition in `ShadowObjectRegistry.cs:262-280`.
|
||||||
|
|
||||||
|
**4. Risk surface:** All changes are additive. No resolver edits. No
|
||||||
|
broadphase edits. No retail-port semantics changes. If anything goes
|
||||||
|
wrong, single-commit revert per task.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Execution
|
||||||
|
|
||||||
|
Plan complete. Two execution options when ready:
|
||||||
|
|
||||||
|
**1. Subagent-Driven (recommended)** — Dispatch a fresh Sonnet subagent per task (Task 1 alone, Tasks 2 + 3 together, Task 4 + 5 + 6 together, Task 7 user-driven, Task 8 docs). Review between dispatches. Each subagent stays bounded to one commit's worth of changes; parent context stays clean.
|
||||||
|
|
||||||
|
**2. Inline Execution** — Drive all tasks in this session using executing-plans. Faster end-to-end but consumes ~4× more parent context.
|
||||||
|
|
||||||
|
Total scope estimate: ~6 commits over ~30–60 minutes of work + Task 7 visual test (~10 minutes when ACE + retail client are already running).
|
||||||
Loading…
Add table
Add a link
Reference in a new issue