Brainstormed spec for the next indoor follow-up phase: surfacing root
causes for ISSUES.md #84 (blocked by air) + #85 (pass through walls
outside→in) + #86 (click selection penetrates walls). Diagnostic-first
single capture pass; one [indoor-bsp] probe in FindEnvCollisions, then
surgical fixes (one commit per issue). Mirrors the indoor cell rendering
Phase 1+2 pattern that landed earlier today.
#86's root cause is already pinned by code reading (WorldPicker has no
cell-BSP test) — its fix is structural and doesn't need capture data.
#78 (outdoor stabs through floor) is in the same handoff cluster but
defers to a separate phase — different code path (render visibility).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six tasks:
1. Add exception-surfacing ContinueWith in WbMeshAdapter.IncrementRefCount
for EnvCell ids when ProbeIndoorUploadEnabled is on. Logs
[indoor-upload] FAILED + [indoor-upload] NULL_RESULT.
2. Capture procedure: user walks Holtburg with the probe on; analyze log.
3. Write cause report documenting the captured exception type(s).
4. Apply targeted fix (4a/4b/4c/4d sub-shapes for the 4 most-likely causes
— choice driven by Task 2's data). Or 4d: re-design if cause is none
of the above.
5. Verification: re-capture confirms completed lines, user visually
confirms floor in Holtburg Inn.
6. Roadmap update.
Tasks 2 and 5 are user-driven (must walk the client). Tasks 1, 3, 4, 6
can be subagent-dispatched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three components:
1. WbMeshAdapter wraps the PrepareMeshDataAsync task with a continuation
that surfaces faulted-task exceptions + null-result cases for EnvCell
IDs only (gated by ProbeIndoorUploadEnabled). Two new log shapes:
[indoor-upload] FAILED cellId=0x... exception=<TypeName>: <Message>
stack=[<top 3 frames>]
[indoor-upload] NULL_RESULT cellId=0x...
2. Capture procedure: re-launch at Holtburg with the probe on, grep for
FAILED/NULL_RESULT lines, get definitive per-cell cause for the 26
missing-completion cells from Phase 1's capture.
3. Targeted fix: code change matching whichever exception type / null
pattern dominates. Fix shape is data-driven — see the contingency
table in the spec.
WB's catch at ObjectMeshManager.cs:589 already calls _logger.LogError,
but WbMeshAdapter constructs the manager with NullLogger.Instance, so
the log is dropped. Our continuation surfaces the same data scoped to
EnvCells only (avoids the thousands of GfxObj/Setup log lines a real
logger would emit during landblock streaming).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: "add the probes you need. Better info, better code."
Original spec had a single ACDREAM_PROBE_INDOOR=1 with vague "log
lookup results" guidance. Replaced with five individually-toggleable
probes, each with:
- Specific env var name + DebugPanel checkbox name.
- Concrete log-line format.
- Exact code site to instrument.
- The hypothesis it disambiguates.
Probe set:
- ACDREAM_PROBE_INDOOR_WALK — dispatcher entity walk per cell
- ACDREAM_PROBE_INDOOR_LOOKUP — render-data lookup hit/miss + SetupParts
- ACDREAM_PROBE_INDOOR_UPLOAD — WB upload result (requested + completed)
- ACDREAM_PROBE_INDOOR_XFORM — composed world transform for cell geom
- ACDREAM_PROBE_INDOOR_CULL — visibility/frustum filter decisions
Plus ACDREAM_PROBE_INDOOR_ALL master toggle.
Implementation outline added: new RenderingDiagnostics static class
(mirrors L.2a's PhysicsDiagnostics pattern), DebugPanel subsection,
edits to WbDrawDispatcher + WbMeshAdapter.
Acceptance criteria refreshed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Initial brainstorm assumed N.5 retirement broke EnvCell rendering by
leaving _pendingCellMeshes unconsumed. Pivoted mid-brainstorm:
- WB's PrepareMeshData routes EnvCell dat-record types to
PrepareEnvCellMeshData (ObjectMeshManager.cs:557) which produces an
IsSetup=true ObjectMeshData with the floor mesh as EnvCellGeometry.
- WbDrawDispatcher correctly handles IsSetup=true (line 607-621) by
iterating SetupParts and drawing each.
- DefaultDatReaderWriter loads region cell dats; ResolveId resolves
envCellId correctly.
- LandblockSpawnAdapter calls IncrementRefCount on every entity's
GfxObjId, including envCellId for cell entities. ServerGuid==0 passes
the atlas-tier filter.
Chain is structurally intact. The bug is somewhere subtler.
Spec pivots to a diagnostics-first phase: ACDREAM_PROBE_INDOOR=1
captures per-frame cell-entity walk + render-data lookup + SetupParts
traversal + composed-transform values. Six hypotheses (WB silently
returns null, empty batches, cull bug, double-spawn, transform
double-apply, dispatcher MeshRefs mismatch) match six concrete fix
shapes. Phase 2 design follows the probe data.
This is more honest than the original "build a new upload path"
design, which would have hidden the actual bug.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8 tasks: CameraDiagnostics static (Task 1) → RetailChaseCamera math
primitives (Task 2) → Update() integration (Task 3) → CameraController
dual-camera (Task 4) → InputAction + DebugVM mirrors (Task 5) →
DebugPanel section (Task 6) → GameWindow wiring (Task 7) → build +
test + visual handoff (Task 8). Each task is TDD-shaped with exact
code in every step. PlayerTranslucency is computed + tested but
applying to the player mesh is explicitly deferred (Q1 escape clause
in the spec).
For docs/superpowers/specs/2026-05-18-retail-chase-camera-design.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Spec for porting retail's CameraManager + CameraSet behavior to a new
RetailChaseCamera class, controlled by a CameraDiagnostics toggle so the
user can A/B against legacy ChaseCamera. Covers six retail behaviors:
exponential damping (stiffness*dt*10), 5-frame velocity-averaged slope
alignment, mouse low-pass (0.25s window), held-key offset integration,
auto-fade <0.45m, and independent translation/rotation stiffness rates.
Brainstormed end-to-end before any code change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#63, #69, #74, #75. Replaces the chain of Commit-B workarounds
that compensated for ACE's MoveToChain getting cancelled by a leaked
user-MoveToState packet during inbound auto-walk. The fix is
architectural — auto-walk drives the body directly from the
server-supplied path data, no player-input synthesis, no spurious
wire-packet transitions, no grace-period band-aid.
Architectural change (closes#75):
PlayerMovementController.ApplyAutoWalkOverlay → DriveServerAutoWalk.
- Steps Yaw toward target at retail-faithful turn rates.
- Computes desired forward velocity from path runRate.
- Calls _motion.DoMotion(WalkForward, speed) directly for the
motion-interpreter state (drives animation cycle).
- Sets _body.set_local_velocity directly when grounded.
- Returns true to gate the user-input motion + velocity section
in Update so user-input flow doesn't overwrite auto-walk
velocity or motion state.
Mirrors retail's MovementManager::PerformMovement case 6 (decomp
0x00524440) which never touches the user-input pipeline during
server-controlled auto-walk.
Wire-layer guard at GameWindow.cs:6419 retained as a SEMANTIC
statement (`if (result.MotionStateChanged && !IsServerAutoWalking)`):
user-MoveToState packets are for user-driven motion intent. During
server-controlled auto-walk, the motion-state transitions caused by
the animation override (RunForward / WalkForward / TurnLeft /
TurnRight cycles) must not leak as user-cancellation packets. This
is NOT the deleted 500ms grace-period band-aid; it's the wire-layer
expressing the user-vs-server motion split.
Animation plumbed for auto-walk phases (closes#69):
- Moving forward → WalkForward (speed=1.0) / RunForward (speed=runRate)
- Turn-first phase → TurnLeft / TurnRight (sign of yawStep)
- Aligned-but-pre-step / arrival → no override (idle)
Driven via _autoWalkMovingForwardThisFrame + _autoWalkTurnDirectionThisFrame
fields set in DriveServerAutoWalk and read in the MovementResult
construction at the bottom of Update. UpdatePlayerAnimation picks up
the localAnimCmd as the highest-priority animation source.
Walk/run threshold = 1.0m, retail-observed. ACE's wire-default of
15.0f is too generous; ACE's own physics layer uses 1.0f at
MovementParameters.cs:50 (with the 15.0f line commented out) and
Creature.cs:312 notes "default 15 distance seems too far". The
formula matches retail's MovementParameters::get_command at decomp
0x0052aa00: running = (initialDist - distance_to_object) >=
threshold, evaluated ONCE at chain start and held for the rest of
the auto-walk (matches retail "runs all the way / walks all the way"
behaviour). Wire-supplied threshold is ignored.
Pickup gate (IsPickupableTarget) now uses BF_STUCK
(acclient.h:6435, bit 0x4) to discriminate immovable scenery from
real pickup items that share a Misc ItemType. Sign (pwd=0x14 with
BF_STUCK) → blocked; spell component (pwd=0x10, no BF_STUCK) →
allowed. ACE's PutItemInContainer (Player_Inventory.cs:831-836)
responds with WeenieError.Stuck (0x29) on stuck items so the gate
prevents wasted wire packets + a UX dead-end.
R-key dispatch by target type. UseCurrentSelection's top-level
IsUseableTarget gate was wrong (blocked USEABLE_NO=1 items that
ARE pickupable). Reordered:
1. Creature → SendUse
2. Pickupable → SendPickUp
3. Useable → SendUse
4. Otherwise → "cannot be used" toast
Each handler keeps its own gate. Matches retail's per-action
server-side validation.
AP cadence revert (closes#74). With the MoveToChain race fixed,
the per-frame "send while moving" cadence is no longer load-bearing.
Reverted to retail's two-branch ShouldSendPositionEvent gate
(acclient_2013_pseudo_c.txt:700233-700285):
Interval NOT elapsed (< 1 sec): send if cell or contact-plane changed.
Interval elapsed (>= 1 sec): send if cell or position frame changed.
Adds _lastSentContactPlane field + ApproxPlaneEqual helper +
PlayerMovementController.ContactPlane public accessor. Extended
NotePositionSent(Vector3, uint, Plane, float) — both outbound sites
(MoveToState + AP) pass _playerController.ContactPlane.
Effective rates: 0 Hz idle, ~1 Hz smooth motion, per-event on
cell/plane changes, 0 Hz airborne.
CLAUDE.md updated with no-workarounds rule (commit `da126f9` on
the worktree branch). Saved as feedback memory at
memory/feedback_no_workarounds.md.
Tests: build green; Core.Net 294/294; Core 1073/1081 (baseline,
8 pre-existing Physics failures unchanged). Visual-verified
end-to-end on 2026-05-16 for far/near Use + PickUp on NPCs,
doors, items, spell components, signs (correctly blocked), corpses,
turn-first animation, run/walk thresholds, idle quiet, smooth-
motion 1Hz.
Spec: docs/superpowers/specs/2026-05-16-phase-b6-suppress-movetostate-during-inbound-autowalk-design.md
Plan: docs/superpowers/plans/2026-05-16-phase-b6-suppress-movetostate-during-inbound-autowalk.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Retires divergences flagged in the 2026-05-16 faithfulness audit:
1. AP cadence. Replaces the 1 Hz idle / 10 Hz active flat heartbeat
with a diff-driven model gated on `Contact && OnWalkable`
(acclient_2013_pseudo_c.txt:700327 SendPositionEvent). Sends on
position or cell change while grounded on walkable, plus a 1 sec
heartbeat; suppressed entirely airborne. PlayerMovementController
exposes `NotePositionSent(pos, cellId, now)` which GameWindow stamps
after each AutonomousPosition / MoveToState send — mirrors retail's
shared `last_sent_position_time` between SendPositionEvent
(0x006b4770) and SendMovementEvent (0x006b4680). Known divergence
from retail: ours is per-frame-while-moving, retail's effective rate
is ~1 Hz during smooth motion (cell/plane checks). Filed as #74,
blocked by #63 — when #63 lands we revert to retail's narrower gate.
2. Workaround retirement. Removes TinyMargin (0.05 m inside arrival)
and the AP-flush before re-send (`SendAutonomousPositionNow`). The
diff-driven cadence makes both obsolete. Close-range turn-first
deferred Use is kept (it IS retail — ACE Player_Move.cs:66-87
mirrors retail's CreateMoveToChain pre-callback rotation), renamed
`OnAutoWalkArrivedSendDeferredAction` to clarify it's a FIRST send.
`isRetryAfterArrival` parameter dropped.
3. Far-range Use/PickUp retry. Restored — was load-bearing, not the
"redundant cleanup" the Group 2 audit thought. Issue #63 means ACE
drops the first Use as too-far without re-polling on subsequent APs;
the arrival re-send is what makes far-range Use complete. Logs
include `(queued for arrival re-send pending #63)` to make this
explicit. Removes when #63 closes.
4. Screen-rect picker. New `AcDream.Core.Selection.ScreenProjection`
helper shared by `WorldPicker` and `TargetIndicatorPanel`. The
`Setup.SelectionSphere` projects to a screen-space square (retail
anchor `SmartBox::GetObjectBoundingBox` 0x00452e20); picker
hit-tests the mouse pixel against the same rect the indicator draws,
inflated by 8 px (`TriangleSize`). Guarantees what-you-see is
what-you-click — including rect corners that were dead zones under
the old ray-sphere picker. Per-type radius (1.0/1.6/2.0 m) and
vertical-offset (0.2/0.9/1.0/1.5 m) heuristic lambdas retired;
`IsTallSceneryGuid` deleted; `EntityHeightFor` trimmed to 1.5 m × scale
defensive default. No defensive sphere synth — entities without a
baked `SelectionSphere` are skipped, matching retail's
`GfxObjUnderSelectionRay` (0x0054c740).
5. Rotation rate run multiplier (Commit A precursor). `TurnRateFor(running)`
helper applies retail's `run_turn_factor = 1.5f` (PDB-named
0x007c8914) under HoldKey.Run, matching `apply_run_to_command` at
0x00527be0 (line 305098). Effective: walking ≈ 90°/s, running ≈ 135°/s.
Keyboard A/D + ApplyAutoWalkOverlay both use it.
6. Useability gate (Commit A precursor). `IsUseableTarget` corrected to
`useability != 0` per `ItemUses::IsUseable` at 256455 — ANY non-zero
passes (USEABLE_NO=1, USEABLE_CONTAINED=8, etc.), not just the
USEABLE_REMOTE bit. Cross-checked against 4 call sites in retail
(ItemHolder::UseObject 0x00588a80, DetermineUseResult 0x402697,
UsingItem 0x367638, disable-button-state 0x198826). Added
`ProbeUseabilityFallbackEnabled` diagnostic
(`ACDREAM_PROBE_USEABILITY_FALLBACK=1`) to measure how often the
creature/BF_DOOR fallback fires for ACE-seed-DB entities with
null useability.
CLAUDE.md updated with the graceful-shutdown rule for relaunch:
Stop-Process bypasses the logout packet, leaving ACE's session marked
logged-in for ~3+ min. CloseMainWindow() sends WM_CLOSE so the
shutdown hook runs and the logout packet reaches ACE.
Tests: +3 ScreenProjectionTests + 6 WorldPickerRectOverloadTests = +9.
Core.Net 294/294 pass; Core 1073/1081 (8 pre-existing Physics failures
unchanged). Visual-verified 2026-05-16: rotation rate, useability,
screen-rect click area, double-click + R-key + F-key Use/PickUp at
short and long range — dialogue/door/pickup fire on arrival.
Filed follow-ups #70 (triangle apex/size DAT sprite), #71 (picker
Stage B polygon refine), #72 (cdb omega.z probe), #73 (retail-message
sweep pattern), #74 (per-frame AP chattier than retail — blocked by
#63). Old ray-sphere `WorldPicker.Pick(origin, direction, ...)`
overload kept for back-compat; no callers in acdream proper.
Plan: docs/superpowers/plans/2026-05-16-retail-faithfulness-fixes.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two retail divergences fixed from the 2026-05-16 faithfulness audit
(Commit A of the plan at docs/superpowers/plans/2026-05-16-retail-faithfulness-fixes.md).
1. Rotation rate ignored HoldKey.Run. Retail's CMotionInterp::
apply_run_to_command (decomp 0x00527be0 line 305098) multiplies
turn_speed by run_turn_factor (1.5, PDB-named symbol at 0x007c8914)
when input is TurnRight/TurnLeft under HoldKey.Run. Effective
running rotation is 50% faster (~135°/s vs walking ~90°/s).
Our keyboard A/D and ApplyAutoWalkOverlay used a fixed walking
rate.
New: RemoteMoveToDriver.TurnRateFor(running) helper. Keyboard
path passes input.Run; auto-walk overlay passes
_autoWalkInitiallyRunning. The walking-rate base
(BaseTurnRateRadPerSec = π/2) is unchanged; TurnRateRadPerSec
constant is preserved as the walking-rate alias for callers
that don't have run/walk state (NPC remotes).
2. IsUseableTarget gated on `useability & USEABLE_REMOTE (0x20)`,
which was stricter than retail. Per ItemUses::IsUseable
(acclient_2013_pseudo_c.txt:256455) cross-referenced with 4
call sites, retail's IsUseable() semantic is `_useability != 0`.
But visually retail's USEABLE_NO (1) entities don't approach
either, because ACE never broadcasts MovementType=6 for them.
Our client installs a speculative auto-walk BEFORE the server
responds, so we'd visibly approach + face signs before the
wire packet was rejected.
Pragmatic fix: block USEABLE_UNDEF (0) AND USEABLE_NO (1) in
IsUseableTarget — slightly stricter than retail's
IsUseable but matches retail's user-visible behaviour
("R on sign does nothing"). Documented in the doc-comment so
a future implementer knows the gap.
3. New IsPickupableTarget gate for F-key path — requires
USEABLE_REMOTE (0x20) bit. Null-useability fallback for
BF_CORPSE + small-item ItemTypes (preserves M1 ground-item
pickup flow when ACE seed DB doesn't publish useability).
4. R-key (UseCurrentSelection) upfront gate now ALWAYS uses
IsUseableTarget. R is conceptually "use" with smart-routing
to pickup as a downstream optimization. F-key (SendPickUp)
uses IsPickupableTarget directly.
5. Retail toast strings on block, centralised in new
src/AcDream.Core/Ui/RetailMessages.cs:
- "The X cannot be used" (data 0x007e2a70, sprintf 0x00588ea4)
fires on UseCurrentSelection / SendUse gate block.
- "The X can't be picked up!" (sprintf 0x00587353) fires on
SendPickUp non-pickupable block.
- "You cannot pick up creatures!" (data 0x007e22b4) fires on
SendPickUp creature block (was previously silent).
- Plus 4 inactive retail strings ready for future call sites:
CannotBeUsedWith (two-target Use), CannotBePickedUp (formal
pickup variant), CannotBeUsedWhileOnHook_HooksOff +
CannotBeUsedWhileOnHook_NotOwner (housing). All cite their
retail data addresses + runtime sprintf addresses.
6. ProbeUseabilityFallbackEnabled diagnostic (env var
ACDREAM_PROBE_USEABILITY_FALLBACK=1) logs every time the
null-useability fallback fires. Settles whether the
fallback for creature + BF_DOOR/LIFESTONE/PORTAL/CORPSE
entries in ACE's seed DB without useability is hot code
or theoretical defense.
Test coverage:
- +3 RemoteMoveToDriverTests cover TurnRateFor walking/running/back-compat.
- +7 RetailMessagesTests cover each retail string with retail anchor.
- +1 CreateObjectTests TryParse_WeenieFlagsUsable_ReadsUseableNoValue
pins parser correctness for USEABLE_NO=1.
- 294/294 Core.Net pass; 24/24 new+touched Core tests pass.
- Pre-existing baseline of 8 Physics test failures unchanged
(BSPStepUp + MotionInterpreter regression noise from prior
sessions; out of scope here).
Deferred to a separate session per user direction:
- Click area = indicator-rect retail fidelity. Retail's picker
uses per-part CGfxObj.drawing_sphere + polygon refine
(0x0054c740); ours uses single Setup.SelectionSphere ray-
intersect. The rect corners are dead zones today. Three fix
options analyzed: screen-space rectangle hit-test, sqrt(2)
sphere inflation, polygon refine Stage B.
Plan: docs/superpowers/plans/2026-05-16-retail-faithfulness-fixes.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Retail-anchored design for the missing visual feedback on selection:
four corner triangles + radar-blip colour coding around the selected
entity, drawn via ImGui in screen space.
Retail evidence (named decomp):
* VividTargetIndicator::SetSelected at 0x004f5ce0
* gmRadarUI::GetBlipColor at 0x004d76f0 (Portal / Vendor / Creature /
Player / PK / PKLite / Default colours from pwd._bitfield bits +
IsCreature/IsPlayer/IsPK predicates we already parse)
* VividTargetIndicator::CopyImage at 0x004f5dd0 (tints a source
bitmap by RGBA)
MVP scope:
1. RadarBlipColors helper (Core, with unit tests)
2. TargetIndicatorPanel (App, ImGui draw via background draw list)
3. Wire to existing _selectedGuid from B.4b
4. ~200 LOC + tests
Deferred to follow-ups: off-screen edge arrow, DAT-loaded sprite (MVP
draws procedurally), mesh-tint highlight, player-option toggle, server
selection-relay.
Pairs with #59 (WorldPicker over-pick): the indicator makes the
mis-pick visible, so the user can clear + reselect even before the
underlying picker is tightened.
Captured a live ACDREAM_PROBE_AUTOWALK trace double-clicking +Je from
~3.5m. Findings folded into the spec's State at design freeze section:
1. Wire parser is correct (matches ACE MoveToObject.Write +
MoveToParameters.Write byte-for-byte).
2. ACE sends mtRun=0.00. Not a parser bug — that's the wire value.
Retail's apply_run_to_command (0x00527BE0) fell back to the
player's own rate; our Slice 2 needs the same fallback chain.
3. Player position never changed during the entire trace — current
behavior is pure no-op on the inbound MoveToObject (literally
ignored, as our code at OnLiveMotionUpdated:3289 suggests).
4. ACE does NOT broadcast UpdatePosition for the local player during
auto-walk. Definitively kills Option C — nothing to blend with.
Local body must drive itself.
The trace validates the spec's Option A path. Slice 2 implementation
can proceed without further wire-format guessing.
Grounded the design in named-retail evidence. MovementManager::Perform
Movement at 0x00524440 case 6 (decomp lines 300628-300648) shows the
retail client's local-side dispatcher for inbound MoveToObject:
unpacks the wire, sets motion_interpreter->my_run_rate, calls
CPhysicsObj::MoveToObject on the LOCAL player's physics body. Same
code path retail used for every creature chasing the player.
Conclusion: Option A (run a local driver against the player's body)
is retail-faithful. Option C (server-position-blend) is a non-retail
shortcut and is now eliminated from consideration.
Re-scoped the spec into 4 slices:
1. ACDREAM_PROBE_AUTOWALK diagnostic baseline (~30 LOC)
2. PlayerMovementController.BeginServerAutoWalk + reuse of
RemoteMoveToDriver against the local player's body (~100 LOC)
3. Animation cycle selection during auto-walk (~20 LOC)
4. Local pickup-animation echo (closes#64, ~10 LOC)
Total ~160 LOC, no new files. All existing acdream infrastructure
(RemoteMoveToDriver, ServerControlledLocomotion, MotionState.MoveTo
Path parsing) is reused; the work is wiring it for _playerServerGuid
in addition to remote guids.
Captures the wire-format facts that are already parsed (MotionState.
IsServerControlledMoveTo + MoveToPath fields), the two gating sites
that drop the inbound MoveToObject for the local player today, and a
three-option solution space (run remote-driver locally, visual tween,
server-position-authoritative blend).
Recommendation: Option C first (smallest blast radius, single-commit
hotfix if ACE's UpdatePosition broadcast cadence is adequate); promote
to Option A only if the trace shows server broadcasts are too sparse
to render smoothly.
Explicitly does NOT implement yet. The 'walks then snaps back' visible
symptom is observed but the mechanism isn't characterized in detail —
the spec calls for a diagnostic-trace session first (ACDREAM_PROBE_
AUTOWALK env var, ~30 LOC) to capture exactly what ACE sends during a
failed auto-walk. The trace decides between Option C (sufficient
position broadcasts) and Option A (need to fill in per-tick locally).
#64 (local pickup animation) is flagged as likely-related — same
OnLiveMotionUpdated:3289 self-echo filter drops both. May fix in
the same B.6 work.
Task-by-task plan with full code in every step, no placeholders.
Task 1: IsDoorSpawn helper + Door registration branch (state-seeded
SetCycle from spawn PhysicsState ETHEREAL bit).
Task 2: [door-cycle] diagnostic in OnLiveMotionUpdated for greppable
verification.
Task 3: Holtburg inn doorway visual test (user-performed).
Task 4: ship handoff + close#58 + roadmap/CLAUDE/memory updates.
Self-review table at bottom maps every spec section to its task(s);
all covered. Companion to spec
docs/superpowers/specs/2026-05-13-phase-b4c-design.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B.4c closes#58 (filed during B.4b ship). When a door's
state flips via ACE Door.ActOnUse, the L.2g chain handles the
SetState collision-bit flip but no UpdateMotion handler ever
animated the door visually. Investigation traced the gap to the
spawn-time registration gate at GameWindow.cs:2692 which requires
a multi-frame idle cycle — doors have no idle.
Design: door-specific spawn-time branch that bypasses the gate,
builds an AnimationSequencer, seeds it with Off (closed) or On
(open) cycle based on spawn PhysicsState. ACE Door.cs:43 sets the
same initial state. ~40 LOC in one file. Reuses the existing
AnimationSequencer + per-frame tick + WB renderer pipeline. No
changes downstream.
Discovered during self-review that the per-frame tick at
GameWindow.cs:7691-7697 unconditionally overwrites ae.Entity.MeshRefs
with sequencer-derived transforms; an empty sequencer would collapse
the door to origin. The state-seeded SetCycle at spawn keeps the
sequencer always producing valid frames. Also documented:
ae.Animation = null is safe because the tick's sequencer branch at
line 7497 never reads it (only the legacy slerp else branch does).
Diagnostic tags renamed from phase-named [B.4c] to durable
[door-anim] / [door-cycle] per Opus reviewer feedback on B.4b.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Original spec placed WorldPicker in src/AcDream.App/Rendering/ and the
test in tests/AcDream.App.Tests/, but AcDream.App.Tests doesn't exist
as a project. Moved to AcDream.Core.Selection where it conceptually
belongs (no App-layer deps; only WorldEntity + System.Numerics) and
where the existing AcDream.Core.Tests project can hold the tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B.4b closes the M1-blocker discovered during the L.2g slice 1
visual test: the input dispatcher fires SelectDblLeft on click but
GameWindow.OnInputAction has no case for any Select* / UseSelected
action, so clicks silently die.
Spec creates the minimum new structure to close the gap:
- New static helper WorldPicker (BuildRay + Pick over WorldEntities)
- Rename _selectedTargetGuid -> _selectedGuid on GameWindow (unifies
combat + interaction selection per retail's single-target model)
- Three switch cases (SelectLeft, SelectDblLeft, UseSelected)
Two further L.2g handoff inaccuracies surfaced during exploration:
WorldPicker and SelectionState do NOT exist in src/ (handoff and
ISSUES #57 both claimed they did). BuildPickUp also doesn't exist;
only BuildUse / BuildUseWithTarget / BuildTeleToLifestone are present.
Spec accounts for the actual state and defers BuildPickUp + SelectionState
class extraction.
Visual verification scenario reuses the L.2g slice 1 reproducibility
recipe: one Holtburg inn doorway log captures both L.2g + B.4b.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
L.2d slice 1.5 ship identified the Holtburg doorway blocker as a closed
Door entity (Setup 0x020019FF) whose PhysicsState.Ethereal bit flips
when the player Uses the door. The L.2d shape-fidelity work doesn't
cover this — the door's collision shape is correct; what's missing is
honoring the *runtime* state change.
L.2g is the new sub-phase that handles it. Scope is narrow:
* Parse inbound GameMessageSetState (0xF74B).
* Plumb the new PhysicsState value into ShadowObjectRegistry's
cached per-entity state so the existing CollisionExemption.IsExempt
already-in-place short-circuit sees up-to-date bits.
* Verify the Holtburg inn-door scenario: walk in blocked, Use door,
walk through, auto-close blocks again after 30s.
* Confirm UpdateMotion (NonCombat, On/Off) drives non-creature
entities (door swing animation).
Why a new L.2 sub-letter (and not B.4 or Door-special-case): the wire
mechanism (SetState flipping Ethereal) is also how ACE handles activated
traps, opened chests, spell projectiles becoming ethereal. Generic
infrastructure with doors as the verification scenario; lane is the
informal sixth "dynamic state."
Roadmap state:
* L.2 plan-of-record adds the L.2g section after L.2f.
* Milestones doc M1 phase list extended `a-f` -> `a-g`.
* CLAUDE.md status pointer + "next phase candidates" list updated to
name L.2g slice 1 implementation as the natural next step.
Risk: low. Wire-byte width has a hex-dump fallback path in slice 1
(holtburger says 12 bytes, ACE writes 16, capture settles it). ETHEREAL
plumbing already exists; we feed it new data. No resolver changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reframes L.2d direction based on ACE BuildingObj.cs:39-52 + named-retail
acclient_2013_pseudo_c.txt:701260: retail's find_building_collisions is
one BSP test on PartArray.Parts[0]. No per-cell walkability. Per-cell
work (find_cell_list, point_in_cell, sphere/box_intersects_cell) is
L.2e territory.
Slice 1 is now a read-only BSP-hit diagnostic that captures full
collision evidence per L.2a [resolve] hit=yes line. Distinguishes 3
hypotheses (wrong BSP loaded / over-registered parts / BSPQuery flaw)
before any behavior change. Slice 2 is the actual fix, scoped from
slice 1's evidence.
Authors: brainstorm session 2026-05-13 (cold-start from L.2a slice
1+2+3 evidence). Predecessor handoff at
docs/research/2026-05-12-l2a-shipped-l2d-handoff.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two-slice phase:
- Slice A: ParticleHookSink applies CreateParticleHook.PartIndex via
SetupPartTransforms.Compute(setup.PlacementFrames). Closes#56.
- Slice B: drop EntityScriptActivator's ServerGuid==0 guard so
dat-hydrated EnvCell statics + exterior stabs fire DefaultScript.
Key reality discovery folded into the spec §3: EnvCell.StaticObjects
are already WorldEntities (via GameWindow.BuildInteriorEntitiesForStreaming),
so no synthetic-ID scheme + no new walker class needed — the handoff's
§4 Q1/Q2 options are mooted by entity.Id being collision-free.
Doc-drift fixes from C.1.5a folded into §8.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Visual verification at the Holtburg Town network portal passed for the
slice's mechanism: 10-hook portal script fires end-to-end with correct
color, persistence, orientation, and multi-emitter dispatch. After the
334f0c6 rotation-seed fix, the swirl is oriented correctly along the
portal's facing instead of world-NS.
Known limitation surfaced during verification and filed as issue #56:
ParticleHookSink ignores CreateParticleHook.PartIndex, so all 10 of the
portal's emitters collapse to the entity root position + identity-rotated
offset, producing a compressed and partly-ground-buried swirl instead of
the multi-tier shape retail renders. Mechanism is correct; per-part
transform handling is the next vfx-pipeline concern (will affect every
multi-emitter PES — slice 2 chimneys/fireplaces in particular).
Documentation changes:
- docs/ISSUES.md: new #56 entry with the captured entity guids
(0x7A9B405B / 0x7A9B4080), script ids (0x3300126D / 0x3300067A),
symptom data, root-cause hypothesis, file pointers, and acceptance
criterion. Notes the blocks-slice-2 relationship explicitly.
- docs/superpowers/specs/2026-05-12-phase-c1.5a-portals-design.md §9:
new limitation #1 documenting the verified PartIndex collapse symptom.
- docs/plans/2026-04-11-roadmap.md: new "C.1.5a" row in the shipped
table referencing the spec, plan, and #56 caveat.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan: docs/superpowers/plans/2026-05-12-phase-c1.5a-portals.md. Four
tasks, TDD-style, each task is a single commit boundary:
1. EntityScriptActivator class + three xUnit tests
2. Wire into GpuWorldState (new optional ctor param + two ?. calls)
3. Construct in GameWindow with resolver lambda
4. Visual verification at Holtburg Town network portal + roadmap update
Spec amendments correct an inaccuracy in the 2026-05-12 commit
(06d7fbd): the activator's call sites live in GpuWorldState
(AppendLiveEntity / RemoveEntityByServerGuid), not directly in
GameWindow as the original spec described. Also fixes the test file
path: tests/AcDream.Core.Tests/... not AcDream.App.Tests/... per the
existing test-project convention. No design changes — same activator,
same trigger condition, same lifecycle ordering.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slice 1 of Phase C.1.5: fire Setup.DefaultScript through the already-
shipped PhysicsScriptRunner on server-spawned WorldEntity create, so
portals (and any other entity with a DefaultScript) emit their retail-
faithful persistent particle effects at spawn time. Reuses the C.1
runner-sink-system-renderer chain end-to-end; one new ~50-line class
(EntityScriptActivator) plus a two-line wiring in GameWindow.
Slice 2 (C.1.5b) will cover EnvCell.StaticObjects + animation-hook
verification; spec landed separately after slice 1 verification passes.
Acceptance: visual confirmation at the Holtburg Town network portal,
side-by-side with retail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final code review of slice 1 flagged one Important issue (the spec's
"zero cost when off" claim for the surface-dump path is technically
violated — _uploadMetadata always writes one dict entry per upload
regardless of env var) plus minor doc/consistency gaps. Applied:
1. Spec §5 "Cost when off": dropped the "Zero" claim; replaced with
"Negligible — one Dictionary write per upload (~30-50 KB at Holtburg)
plus a hash-table write per upload. Expensive work (file I/O,
histogram construction) is still env-gated." This matches reality.
2. Baseline doc §5: rewrote from "Raw logs (scratch, can be deleted)"
referencing files that were never preserved in this worktree, to
"Reproducing the measurements" with the actual PowerShell launch
commands. Honest about the raw logs not being kept; the captured
medians in section 2 are the canonical record.
3. New issue #55 filed in docs/ISSUES.md — static-entity slow path
reports ~1.45M meshMissing/5s at r4 standstill, drops to ~0 when
walking. LOW severity (no visible regression), hypothesis points
at a "permanently-missing entity gets re-classified every frame"
pattern that Tier 1 cache doesn't cover.
4. Roadmap shipped table: renamed "N.6.1" row to "N.6 slice 1" to
match every other artifact's naming. Search-discoverability fix.
None of these change the slice's conclusion or next-phase
recommendation (C.1.5 first, then reduced-scope slice 2).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step-by-step plan for the two-commit slice: fix WbDrawDispatcher's
gpu_us double-buffering bug (ring-of-3 query slots, read-before-overwrite,
vendor-neutral) then capture the radius=12 baseline at Holtburg with
the now-working diagnostic. Includes exact old_string/new_string Edit
patterns for every code change, PowerShell launch + measurement
procedure for the manual baseline, baseline doc template with explicit
fill-in slots, and a per-criterion acceptance checklist.
Output companion to docs/superpowers/specs/2026-05-11-phase-n6-slice1-design.md
(commit 05d590c).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brainstormed design for the first slice of Phase N.6 (perf polish).
Slice 1 ships two commits: (1) fix the GPU timing query double-buffering
in WbDrawDispatcher (cross-vendor ring of 3, read-before-overwrite),
(2) add an env-gated surface-format histogram dump + capture the
radius=12 perf baseline at Holtburg. Slice 2 (TextureCache cleanup +
shader migration + optional persistent-mapped buffers) is deferred
until after C.1.5 (PES emitter wiring), with the next-phase decision
to be made on the baseline numbers slice 1 produces.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Post-A.5 polish Priority 3. EntityClassificationCache keyed by
(entityId, landblockHint) tuple. Entity dispatcher cpu_us median ~1.2 ms,
p95 ~1.5 ms — ~66% reduction vs pre-Tier-1 baseline. Closes the
post-A.5 polish phase entirely (#52, #54, #53 all closed).
See docs/ISSUES.md #53 closure + memory/project_tier1_cache.md for the
24-commit chain, 4 bug-fix iterations, and the per-tuple-vs-per-entity
recurring trap pattern documented for future cache work.
Captures Phase M (Network Stack Conformance) as a fully-formed phase
ready to be picked up later. Three deliverables:
1. Design spec at docs/superpowers/specs/2026-05-10-phase-m-network-stack-design.md
(~700 lines, 8 sections):
- Bar C completeness target ("wireable on demand"): every wire opcode
a 2013 EoR retail client receives or sends gets a parser/builder +
golden-vector test + typed event in the new layered stack.
- Three-layer architecture: INetTransport / IReliableSession /
IGameProtocol, with WorldSession as a thin behavior consumer.
Concrete C# interface signatures, sub-component decomposition.
- Worktree-branch big-bang migration on claude/phase-m-network-stack;
weekly rebase cadence; single --no-ff merge ships the phase.
- Per-sub-phase entry/exit gates, conformance test plan (golden vectors
+ live capture replay + live ACE smoke), 10-row risk register, scope-
cut order if calendar compresses.
- Cost: 256 hours / ~6.4 weeks single-developer; 4-6 weeks calendar
with subagent parallelization on M.1 + M.6.
2. Opcode coverage matrix at docs/research/2026-05-10-phase-m-opcode-matrix.md
(~284 rows across 5 sections):
- Section 1: 22 transport flags (14 implemented).
- Section 2: 12 optional-header fields (10 partial).
- Section 3: 51 top-level GameMessages (21 implemented).
- Section 4: 103 GameEvent sub-opcodes inside 0xF7B0 (27 parsed,
26 wired).
- Section 5: 96 GameAction sub-opcodes inside 0xF7B1 (24 built,
8 with live callers).
- Roll-up: ~34% complete by raw opcode count. Biggest single
unblocking step is wiring the 16 dead builders in section 5
(Phase B.4 surface — Use / UseWithTarget / Allegiance / Inventory
/ Social / Cast / Appraise).
- Sources cited per row: holtburger (629695a), ACE, named retail
decomp, acdream current state.
- Produced by 4 parallel research agents (one per class). Spot-check
pass owed before M.1 closes.
3. Roadmap update: Phase M section trimmed to summary + status + pointer
to the spec; the previously-tracked M.0 Tier 1 quick-wins are folded
into M.3 / M.4 / M.6 per the spec; M.1 retained as the matrix
construction sub-lane with status note.
Why this shape: the user goal is a complete, layered, testable network
stack that can be wired in as gameplay phases need it — independent of
whether each opcode is yet hooked to game state. The matrix is the
source of truth for "done"; the spec is the architecture the matrix
implements against; the roadmap is the index that points at both.
Decisions captured during the design discussion (in case they need
revisiting):
- Bar C ("wireable on demand") chosen over Bar A (holtburger parity)
or Bar B (named-retail completeness).
- Three layers (INetTransport / IReliableSession / IGameProtocol)
chosen over holtburger's two-layer split.
- Big-bang on a feature branch (worktree) chosen over strangler
pattern; preserves live-ACE testing on main throughout the phase.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The audit at docs/research/2026-05-10-tier1-mutation-audit.md enumerates
every entity.MeshRefs write site (5 STATIC at hydration, 1 DYNAMIC at
GameWindow.cs:7580 inside TickAnimations) and verifies that all 7
Position/Rotation write sites only touch entities in _animatedEntities.
Establishes the load-bearing invariant: an entity's renderer state is
stable from spawn to despawn iff entity.Id is NOT in _animatedEntities.
The spec at docs/superpowers/specs/2026-05-10-issue-53-tier1-cache-design.md
locks in the design from brainstorming on 2026-05-10:
- Static-only cache + DEBUG cross-check (option c) — catches future
regressions of the prior bug class without paying perf cost in Release
- Separate EntityClassificationCache class injected into WbDrawDispatcher
- Cache the rest pose, not the full model matrix (Position/Rotation read
live each frame so Release stays correct even if the invariant breaks)
- Pre-flatten Setup multi-parts at populate time (the bulk of the win)
- 15 new tests covering all invalidation paths + DEBUG cross-check +
Setup pre-flatten + lifecycle pin
Closes the audit + design steps of the post-A.5 polish Priority 3 work.
Implementation plan owned by superpowers:writing-plans next.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code-quality review followup on Task 2 (becbde6) — addresses I1 (the
forward-looking concern that Tasks 3-9's inner-catch will leave partial
lists visible to callers with no signal) and M1 (silent inner catch).
Changes:
- Parsed gains a trailing `bool TrailerTruncated` field. Both
construction sites pass `false` by default; the trailer try/catch
flips a local `trailerTruncated` to `true` on FormatException and
feeds it into the final return.
- Inner catch logs `pos`/`payload.Length`/exception message under
ACDREAM_DUMP_VITALS=1, mirroring the outer catch's diagnostic
pattern.
- Task 2 test strengthened to assert defaults on Options2 /
SpellbookFilters / HotbarSpells / DesiredComps / GameplayOptions /
Equipped + TrailerTruncated=false (M2 followup — gives Tasks 3-9
a regression guard if they consume into the wrong field).
- New test `TryParse_TrailerAbsent_LessThan8BytesAfterEnchantments_*`
documents the contract that <8 bytes after enchantments means the
trailer is absent (not truncated): TrailerTruncated stays false,
upstream attribute data survives.
- Plan updated in lockstep so Tasks 3-11 implementers see the
`trailerTruncated` local and the new return-arg position.
271/271 AcDream.Core.Net.Tests pass.
Code review nit-fix on top of d3b58c9 — addresses two issues from the
quality review of Task 1:
I1 (Important): the record struct `Shortcut` was a homograph with the
flag member `CharacterOptionDataFlag.Shortcut`. Both names live inside
`PlayerDescriptionParser`'s scope. Rename to `ShortcutEntry` aligns
with `InventoryEntry`/`EquippedEntry` and removes the trap before
Task 3's walker references both names in the same method body.
M2 (Minor): `EquippedEntry` had no holtburger source citation; added
one referencing events.rs:180-190. Also expanded `InventoryEntry`'s
comment with the strict reader's validation reference.
Plan doc updated in lockstep so Task 3+ implementers see the new name.
8/8 PlayerDescriptionParser tests still pass.
TerrainModernRenderer replaces TerrainChunkRenderer. Single global
VBO/EBO + slot allocator + glMultiDrawElementsIndirect. Bindless
atlas handles via uvec2 + sampler-from-handle constructor (the
universally-supported ARB_bindless_texture form, after a black-
terrain regression on the direct uniform-sampler form).
Path C: WB renderer pattern + acdream's LandblockMesh.Build for
retail's FSplitNESW formula compliance. Closes issue #51.
Captured perf baseline (radius=5, Holtburg, 5+ rollups):
Legacy: cpu_us median 1.5 / p95 3.0 (1 chunk = 1 glDrawElements)
Modern: cpu_us median 6.4-7.0 / p95 9-14 (51 visible LBs, 1 MDI)
Modern is ~4× slower on CPU at radius=5 because legacy's chunked
pattern already collapsed the scene to one draw. Architectural wins
(zero glBindTexture/frame; constant-cost dispatch as A.5 raises
radius) manifest at higher scene complexity. Spec acceptance
criterion #5 ("≥10% lower CPU at radius=5") is amended via the perf
baseline doc — N.5b ships on visual identity + structural correctness.
Three high-value gotchas captured to memory:
1. `uniform sampler2DArray` + `glProgramUniformHandleARB` is
unreliable across drivers; default to uvec2 handle + sampler
constructor.
2. Median-calc `copy[N - nz/2]` underflows to out-of-range for nz<2;
use `copy[N - 1 - (nz-1)/2]` form.
3. Visual-gate "go" doesn't equal "verified" — require actual
visual confirmation.
Visual verification: confirmed at Holtburg town. 114/114 tests pass
in N.5+N.5b filter. Conformance sentinel max ‖Δ‖ = 0.015 mm across
1000 sample points / 10 representative landblocks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reframe the selection-blink follow-up so it doesn't suggest near-term
work. Was listed in N.5 ship record as "Phase B.4 follow-up adds the
field" — now phrased as open backlog with the hook reserved in
mesh_modern.vert's InstanceData comment for whoever eventually picks
it up.
The shader hook itself is unchanged — change is purely doc wording in
the plan SHIP record + CLAUDE.md WB integration cribs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final cross-cutting review of N.5 found that Task 15's deletion of
mesh_instanced.vert/.frag left InstancedMeshRenderer orphaned —
ACDREAM_USE_WB_FOUNDATION=0 silently rendered terrain+sky only with
no entities. The SHIP commit's "[x] ACDREAM_USE_WB_FOUNDATION=0 still
works" claim was inaccurate.
Resolution: formal retirement of the legacy renderer path within N.5
instead of deferring to N.6.
Deleted:
- src/AcDream.App/Rendering/InstancedMeshRenderer.cs
- src/AcDream.App/Rendering/StaticMeshRenderer.cs
- src/AcDream.App/Rendering/Wb/WbFoundationFlag.cs
GameWindow simplified — capability detection is unconditional, missing
bindless throws NotSupportedException with a clear message at startup.
WbDrawDispatcher + mesh_modern shader load are mandatory after init.
No escape hatch.
GpuWorldState simplified — WbFoundationFlag.IsEnabled guards on
AddLandblock/RemoveLandblock removed; adapter calls are unconditional
when the adapter is non-null.
PendingSpawnIntegrationTests updated — WbFoundationFlag.ForTestsOnly_ForceEnable
static ctor removed (flag is gone; adapter calls are unconditional).
The ApplyLoadedTerrain physics-data loop was also simplified: the
EnsureUploaded sub-loop that fed InstancedMeshRenderer is gone;
_pendingCellMeshes is now explicitly cleared to prevent unbounded
accumulation (the worker thread still populates it, but WB handles
EnvCell geometry through its own pipeline).
Spec §2 Decision 5 + §10 Out-of-Scope updated. Plan ship-amendment
section added. Roadmap updated (N.5 ships with retirement; N.6 scope
narrowed to perf-only). CLAUDE.md "WB integration cribs" updated.
Perf baseline doc updated. WbDrawDispatcher class summary docstring
corrected to describe the as-shipped SSBO + multi-draw-indirect path.
ISSUES.md #51 updated (terrain not in N.5 scope; deferred to N.7).
Bindless support is now a hard requirement. Modern desktop GPUs
universally expose GL_ARB_bindless_texture + GL_ARB_shader_draw_parameters;
if a user hits the NotSupportedException, that's a real bug report
worth investigating, not a silent fallback.
Build: 0 errors, 0 warnings. Tests: 71/71 (Wb+MatrixComposition+TextureCacheBindless filter).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Records the as-shipped state: acceptance gate verdicts, plan amendments
captured during execution, code-review adjustments per task, out-of-scope
N.6 follow-ups, and a complete files-changed summary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code quality review caught:
- sizeofDEIC was a local; promoted to public const DrawCommandStride
so tests can reference it symbolically.
- BatchDataPublic layout invariant (size + field offsets) wasn't
asserted in tests. Added BatchDataPublic_LayoutMatchesPrivateBatchData
+ DrawCommandStride_MatchesStructSize tests to gate Task 10's
MemoryMarshal.Cast<BatchData, BatchDataPublic> safety.
- Plan doc updated: BatchDataPublic spec was Pack=4 (wrong — must
match private BatchData's Pack=8 for the cast to work). Implementation
was already correct; plan now matches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Original Task 5 draft used hardcoded vec3 ambient/sun uniforms in
mesh_modern.frag. Reading actual mesh_instanced.frag revealed it uses
a SceneLighting UBO at binding=1 with 8 lights, fog params (start/end/
lightning/mode), fog color, camera/time, and per-channel clamp.
Revised: mesh_modern.frag preserves the full SceneLighting UBO +
accumulateLights + applyFog + lightning flash + per-channel clamp.
mesh_modern.vert adds vWorldPos output (consumed by accumulateLights
and applyFog). Visual identity to N.4's lighting model preserved.
Two-pass alpha-test (N.5 Decision 2) sits inside the same shader,
gated by uRenderPass instead of uTranslucencyKind.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Original Task 3 had Bindless* methods calling the legacy Texture2D
GetOrUpload* and converting the GL name to a bindless handle —
producing a sampler2D texture sampled via sampler2DArray (GLSL type
mismatch).
Revised: Task 3 introduces three parallel cache dictionaries
(_bindlessBySurfaceId / _bindlessByOverridden / _bindlessByPalette)
storing both the GL texture name and the resident handle. Bindless*
methods call DecodeFromDats + UploadRgba8AsLayer1Array directly with
their own caching; legacy three-cache structure mirrored exactly.
Task 4 (Dispose) updated to: (1) MakeNonResident on every bindless
handle FIRST, (2) DeleteTexture on every Texture2DArray name, (3)
DeleteTexture on every legacy Texture2D handle. Order matters per
ARB_bindless_texture spec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>