Closes#99 (run-through doors regression from b3ce505).
The b3ce505 stopgap for #98 gates the outdoor 24m radial sweep on indoor
primary cells. Combined with ShadowObjectRegistry.GetNearbyObjects'
"skip outdoor ids" filter on the cellScope-pass loop, this meant doors
registered at outdoor cells (default cellScope=0u for server-spawned
entities at GameWindow.cs:3139) were invisible to spheres on the indoor
side of a doorway threshold — walk-through.
Pre-flight reads found that CellTransit.FindCellSet already adds
outdoor cells to its candidate set when the sphere straddles an
OtherCellId=0xFFFF exit portal (via AddAllOutsideCells triggered by
exitOutside=true inside the indoor-seed BFS). The fix is to stop
filtering those outdoor ids out before iterating, and rename the param
to portalReachableCells to reflect what the set actually contains.
- Q1: Indoor EnvCell.VisibleCellIds is indoor-only in all 16 cottage
fixtures (low 16 bits ≥ 0x0100). OtherCellId=0xFFFF on portals
marks "exit to outdoor world" without naming a specific cellId; the
specific outdoor cell is computed by AddAllOutsideCells from world
XY when the sphere straddles the exit portal.
- Q2: GameWindow.cs:3139 ShadowObjects.Register for server-spawned
entities passes no cellScope → default 0u → outdoor 24m grid
registration. UpdatePosition (line 145) does the same on movement.
Doors are confirmed outdoor-registered.
Slice 1 makes a smaller change than the spec proposed (no new
parameter; just drop the existing filter), because FindCellSet's
existing exit-portal logic already exposes the needed outdoor cells.
The retail-faithful registration-side BuildShadowCellSet refactor and
the b3ce505 gate removal stay scheduled for slices 2-3.
Verification:
- 24/24 ShadowObjectRegistryTests pass (incl. two new slice 1 tests:
IndoorPrimary_OutdoorCellInPortalSet_DoorReturned closes#99;
IndoorPrimary_IndoorOnlyPortalSet_OutdoorRadialStillSkipped
regression-pins #98)
- 11/11 CellarUpTrajectoryReplayTests pass (LiveCompare_FirstCap_
FixClosesCottageFloorCap stays green)
- dotnet build AcDream.slnx: 0 errors, 0 warnings
- Pre-existing 6-8 static-state-leakage failures in serial physics
suite verified unchanged by stash+retest baseline check
Visual verification pending: walk Holtburg cottage doorway from both
sides; door blocks both directions; cellar still climbable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The cellar-up cap was caused by ShadowObjectRegistry.GetNearbyObjects
running its outdoor 24m-grid radial query unconditionally — including
when the moving sphere's primary cell is indoor. The landblock-baked
cottage GfxObj 0x01000A2B (registered with cellScope=0u, i.e.
landblock-wide) was returned for a sphere inside the cellar EnvCell,
and its downward-facing cottage-floor poly at world Z=94 head-bumped
the sphere from below, capping ascent at foot Z=92.74.
Diagnosis this session via the live capture in
a6-issue98-resolve-capture-2.jsonl (92K records, 132 cap events all
with body on the ramp polygon) FALSIFIED the prior "stale ramp
contact plane" hypothesis: the contact plane is correctly the ramp's
plane because the sphere IS on the ramp at the cap. The cap is a
proximate consequence of the cottage GfxObj being queried at all from
an indoor primary cell.
Retail decomp anchor (acclient_2013_pseudo_c.txt):
- 308751-308769: CObjCell::find_cell_list branches on the moving
object's m_position.objcell_id — INDOOR adds only that cell +
portal-visible neighbors via CELLARRAY::add_cell; OUTDOOR adds
all overlapping outdoor cells via CLandCell::add_all_outside_cells.
Object-position-driven, not sphere-radius-driven.
- 309560: CEnvCell::find_collisions calls find_env_collisions
(own cell BSP only) THEN CObjCell::find_obj_collisions on `this`.
- 308916: CObjCell::find_obj_collisions iterates this->shadow_object_list
— strictly per-cell, never landblock-wide.
Combined: a landblock-baked static like the cottage building is added
to outdoor cells' shadow_object_list only (its m_position resolves to
an outdoor cell). An indoor EnvCell's shadow_object_list never
contains the cottage. CEnvCell::find_collisions therefore never tests
the sphere against the cottage. Retail-faithful behavior.
Falsification spike (this session): scoping the cottage to a single
distant outdoor cell instead of landblock-wide caused the harness
LiveCompare_FirstCap test to stop reproducing the cn=(0,0,-1) cap,
confirming the cap is caused by the radial sweep returning the
cottage to an indoor primary.
The fix:
- Add optional `primaryCellId` parameter to
ShadowObjectRegistry.GetNearbyObjects. When indoor (>= 0x0100),
skip the outdoor radial sweep entirely after the indoorCellIds
branch runs. Default 0u preserves prior behavior for
cell-unaware callers (existing tests pass unchanged).
- Transition.FindObjCollisions passes sp.CheckCellId.
- Harness LiveCompare_FirstCap_* flipped to documents-the-fix form
(asserts the downward-facing cottage-floor cap does NOT fire).
Deletes the residual-X-motion test that documented a post-cap
edge-slide — irrelevant once the cap is gone.
This same gate should close the other "Finding 3 family" indoor/outdoor
collision bugs (#97 phantom collisions, indoor sling-out). Visual
verification by the user is the remaining acceptance check before
closing #98.
Verification:
- 11/11 CellarUpTrajectoryReplayTests pass in isolation
- 55 ShadowObjectRegistry + TransitionTypes + PhysicsEngine
+ CellPhysics + CellTransit tests pass
- 8 pre-existing static-state-leakage failures in serial physics
suite are unchanged (verified by stash + retest on baseline)
- dotnet build clean, 0 warnings
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Interior items (fireplaces, tables, chests) registered via A1.5's
ShadowObjectRegistry.Register `cellScope` parameter (commit 4d3bf6f)
are stored under their ParentCellId key (e.g. 0xA9B40121). But
GetNearbyObjects's broad-phase only iterates outdoor 24m landcell
keys (0xA9B40029 etc) and never looks up indoor cell keys, so
interior shadows were registered but unreachable. User-visible
symptom: tables/boxes/fireplaces don't block movement, while walls
DO block (the indoor BSP path is separate).
Fix: GetNearbyObjects accepts an optional indoorCellIds parameter
and additionally queries _cells[indoorCellId] for each entry with
low-byte >= 0x0100u. FindObjCollisions computes the set via
CellTransit.FindCellSet (same set A4 uses for multi-cell BSP
iteration) and passes it through. Outdoor seeds typically produce
sets containing only outdoor land-cells which the new branch filters
out, so the outdoor-only behavior is preserved.
1147 + 8 baseline maintained. Closes the user-reported regression
"walls block now correct but interior items such as tables and boxes
or fireplaces do not block."
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ISSUES #83 Phase A1.5. ShadowObjectRegistry.Register() assigned each
entity to the outdoor landcell grid (8x8 cells, 24m square) based on
its XY position. For interior EnvCell statics (fireplace, furniture,
sign) hydrated by BuildInteriorEntitiesForStreaming with
ParentCellId = envCellId (a high-cellId interior cell like
0xA9B40121), this meant the shadow got stamped into the OUTDOOR
landcell whose XY they overlapped (e.g., 0xA9B40029).
When the player was OUTSIDE the building in 0xA9B40029, the indoor
chair/fireplace shadow fired collisions in "thin air" outdoors. The
user reported this on Holtburg cottage exteriors after the Phase A1
landblock-stab fallback fix.
Fix: add optional cellScope parameter to Register(). When non-zero
(passed as entity.ParentCellId ?? 0u from the 5 entity-loop call
sites in GameWindow), skip the XY-based landcell loop and register
the shadow ONLY in that cell. Live server-spawn registration at
GameWindow.cs:3137 keeps the XY-based behavior (live entities move
between cells).
Probe evidence (launch-a1-verify.utf8.log, post-A1 capture):
- 71 hits on 0x40B50054 (interior static) in OUTDOOR cell 0xA9B40029.
- 47 hits on 0xA9B47C00 (other Holtburg cottage BSP — legitimate).
- 31 hits on 0x40B50048 / 15 on 0x40B50018 (interior statics).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Plumbing-only foundation for the upcoming live-entity (NPC / monster
/ player) collision port. No behavior change — the new fields default
to zero/None so the 5 existing static-entity Register call sites in
GameWindow.cs are untouched.
Wire layer:
- CreateObject parser now surfaces PhysicsState (acclient.h:2815 —
ETHEREAL_PS=0x4, IGNORE_COLLISIONS_PS=0x10, HAS_PHYSICS_BSP_PS=0x10000,
...) which the parser previously dropped at line ~337 with a bare
`pos += 4`.
- CreateObject parser now surfaces ObjectDescriptionFlags (the retail
PWD._bitfield trailer per acclient.h:6431-6463), where
acclient_2013_pseudo_c.txt:406898-406918 ACCWeenieObject::IsPK /
IsPKLite / IsImpenetrable read bits 5 / 25 / 21 directly. Previously
read-and-discarded.
- WorldSession.EntitySpawn carries both new fields through to subscribers.
Physics layer:
- New `EntityCollisionFlags` enum (IsPlayer / IsCreature / IsPK /
IsPKLite / IsImpenetrable) + `FromPwdBitfield` helper. Bit
positions verified against retail's SetPlayerKillerStatus (
acclient_2013_pseudo_c.txt:441868-441890) which maps
PKStatusEnum→bitfield exactly: PK=0x4→bit5, PKLite=0x40→bit25,
Free=0x20→bit21.
- `ShadowEntry` extended with `State` (raw PhysicsState bits) +
`Flags` (decoded EntityCollisionFlags). Backward-compatible — all
five existing landblock-entity Register call sites omit them.
- `ShadowObjectRegistry.UpdatePosition(entityId, pos, rot, ...)` —
fast-path for the 5–10 Hz UpdatePosition (0xF748) stream the server
emits per visible entity. Reuses the entry's existing shape +
state + flags. Mirrors retail's CPhysicsObj::SetPosition
(acclient_2013_pseudo_c.txt:284276) which keeps the same shape and
re-registers cell membership.
- `ObjectInfoState` adds `IsPK = 0x800` and `IsPKLite = 0x1000`
matching retail's OBJECTINFO::state bits (acclient.h:6190-6194).
Used by Commit C's PvP exemption gate.
Tests:
- `EntityCollisionFlagsTests` — 7 tests covering empty / each bit
alone / PK+player combo / unrelated-bit ignore.
- `ShadowObjectRegistryTests` — 5 new tests: UpdatePosition moves
entry to new cell, preserves State/Flags, unregistered no-op,
Register stores State/Flags, defaults are zero/None.
- `CreateObjectTests` — 3 new tests verifying PhysicsState + PWD
bitfield (with PK / PKLite bit cases) parse and surface.
1454 → 1454 + 15 = covered by suite. dotnet build + dotnet test
green.
Foundation for Commit B (live-entity registration) and Commit C
(PvP exemption block in FindObjCollisions).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the first on-screen HUD for the dev client plus today's mouse-control
refinements. Also lands yesterday's scenery-alignment changes that were
left uncommitted in the working tree.
Overlay:
- BitmapFont rasterizes a system TTF via StbTrueTypeSharp into a 512x512
R8 atlas at startup (Consolas on Windows, DejaVu/Menlo fallbacks)
- TextRenderer batches 2D quads in screen-space with ortho projection;
one shader + two draw calls (rect then text) for panel backgrounds
under glyphs
- DebugOverlay composes info / stats / compass / help panels on top of
the 3D scene; toggles via F1/F4/F5/F6; transient toasts for key events
- DebugLineRenderer and its shaders (carried over from the scenery work)
are properly committed in this commit
Controls:
- Per-mode mouse sensitivity (Chase 0.15, Fly 1.0, Orbit 1.0); F8/F9 to
adjust the active mode multiplicatively (x1.2)
- Hold RMB to free-orbit the chase camera around the player; release
stays at the new angle (no snap-back)
- Mouse-wheel zooms chase distance between 2m and 40m
- Chase pitch widened to [-0.7, 1.4] so mouse-Y tilts both ways from
the default neutral angle
Scenery alignment (carried from yesterday's session):
- ShadowObjectRegistry AllEntriesForDebug + Scale field
- SceneryGenerator uses ACViewer's OnRoad polygon test + baseLoc +
set_heading rotation
- BSPQuery dispatchers accept localToWorld so normals/offsets transform
correctly per part
- TransitionTypes.CylinderCollision rewritten with wall-slide + push-out
- PhysicsDataCache caches visual-mesh AABB for scenery that lacks
physics Setup bounds
GetNearbyObjects now searches the player's landblock plus all 8
neighbors. Previously only searched one landblock, missing objects
near landblock boundaries — which includes most trees/rocks since
scenery is placed across the full streaming window.
Also added diagnostic logging (will strip after verification).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Most scenery objects (trees, rocks) use CylSphere collision from
their Setup, not PhysicsBSP. Register these in ShadowObjectRegistry
with a Cylinder collision type. FindObjCollisions now handles both:
- BSP: full polygon collision via BSPQuery (buildings, stabs)
- Cylinder: radial + vertical cylinder-sphere test (trees, NPCs)
Diagnostics showed 170 CylSphere entities vs 278 BSP entities in
the Holtburg landblock alone — this roughly doubles collision coverage.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace simplified push-out with retail-faithful SlideSphere and
AdjustOffset from transition_pseudocode.md. Crease-projection between
collision normal and contact plane produces smooth wall-sliding.
Object collision uses proper rotation transform to object-local space.
SlideSphere (section 6): computes crease direction via cross product
of collision normal and contact plane normal, projects displacement
onto the crease, then applies the correction offset. Handles three
cases: crease exists, parallel same-direction, parallel opposing.
AdjustOffset (section 6): adds safety check to keep sphere above
contact plane by computing signed distance and pushing up along Z
when the sphere dips below.
FindObjCollisions: removes ad-hoc penetration push-out, now calls
SlideSphere after BSP hit detection for proper wall-slide behavior.
Also fixes: ShadowEntry gains Rotation field, tests updated to match
Register signature, unused variables removed from GameWindow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Register static entities into terrain cells during streaming.
Transition system queries nearby objects and runs BSP collision.
Player can no longer walk through trees and buildings.
- ShadowObjectRegistry: 24m×24m cell index, Register/Deregister/
RemoveLandblock/GetNearbyObjects matching retail AC's approach
- PhysicsEngine: ShadowObjects property + DataCache wiring point;
RemoveLandblock now also clears shadow objects; TryGetLandblockContext
helper lets Transition resolve landblock id+offset for a world pos
- Transition.FindObjCollisions: queries registry, broad-phase sphere test,
narrow-phase BSPQuery.SphereIntersectsPoly in object-local space,
returns Slid on hit to redirect movement along the surface
- GameWindow.ApplyLoadedTerrainLocked: registers each static entity after
physics BSP data is cached; selects radius from BSP bounding sphere or
Setup.Radius; wires PhysicsDataCache into engine on OnLoad
- 16 new ShadowObjectRegistry unit tests, all 361 tests green
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>