fix(physics): AP-10 - restore retail's 0.1m dry-corner water sink-in; wire WATER_CONTACT_TS

Campaign P Slice P4 item 2. TerrainSurface.SampleWaterDepth now returns 0.1
(was collapsed to 0) for a partially-water cell's dry corner, matching
retail's ObjCell.get_water_depth / calc_water_depth (via ACE's unambiguous
C# port). ValidateWalkable's formula was already byte-for-byte verbatim
(ACE ObjectInfo.ValidateWalkable line 124); only the constant was collapsed.

The old collapse's justification ("0.1 destabilizes the feet-exactly-on-plane
contact-touch check because dist > EPSILON skips SetContactPlane that tick")
is structurally true of retail too - traced and confirmed this slice: in ALL
THREE implementations (retail, ACE, acdream) a skipped touch-reassertion is
NOT a fall, because Contact/OnWalkable are STICKY -
PhysicsEngine.ResolveWithTransition's onGround computation ORs the fresh
per-call ContactPlaneValid with the seeded, persistent
PhysicsBody.TransientState.OnWalkable bit (itself written back by the
caller's own sticky TransientState). PhysicsEngine.SampleTerrainWalkable's
isWater = waterDepth >= 0.45f threshold means the restore does not flip the
dry corner's water classification (0.1 still < 0.45) - only the sink-in
depth changes. Full Core.Tests suite green (4038/2 skips, up from 4026)
proves the sticky-bit argument held in practice.

WATER_CONTACT_TS (TransientStateFlags.WaterContact, declared but never
written) is now mirrored alongside CONTACT_TS/ON_WALKABLE_TS at every commit
point that writes them: PhysicsObjUpdate.ApplySetPositionContact (projectiles
+ remote teleport), PhysicsObjUpdate.CommitSetPositionTransition (remote
teleport placement), and PhysicsEngine's per-resolve body-state commit (local
player + remote dead-reckoning + ordinary movers via ResolveWithTransition -
the actual SetPositionInternal-equivalent path). No signature changes needed:
body.ContactPlaneIsWater is already fresh by the time each function runs.

CollisionShadowVerifier audit: no change needed. It diffs graph-vs-flat BSP
traversal outcomes (ObjectInfo/CollisionInfo/SpherePath fields already
including ContactPlaneIsWater); it never touches PhysicsBody.TransientState,
and the water-depth constant is computed identically upstream of both
traversal modes, so it cannot introduce a new graph/flat divergence.

Filed #264 for the three items research explicitly left open (none block
this port): no confirmed retail consumer of WATER_CONTACT_TS was found (an
xref scan wasn't attempted - bitmask reads aren't text-greppable); the
CLandCell ENTIRELY_WATER ethereal/swim exemption from terrain collision was
not cross-checked; jump-in-water/swim-animation effects were not
investigated (out of physics/collision scope).

Conformance: Ap10WaterSemanticsTests covers SampleWaterDepth golden values
(NotWater/EntirelyWater/PartiallyWater wet+dry corners), the isWater
threshold non-flip, WaterContact mirroring in both PhysicsObjUpdate
functions, and two settle-to-rest end-to-end PhysicsEngine.ResolveWithTransition
scenarios (water: sinks exactly waterDepth below the plane and sets
WaterContact; dry: rests exactly on the plane and clears any stale
WaterContact bit).

Register: retired AP-10 (92 active AP rows, down from 93).

AcDream.Core.Tests: 4038 passed, 2 skipped, 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-30 10:52:39 +02:00
parent d6c3f8657a
commit cc8d57a26e
7 changed files with 439 additions and 18 deletions

View file

@ -147,6 +147,54 @@ regressing the #225 lifestone/candle compositing.
---
## #264 — Water semantics: WATER_CONTACT_TS consumer + two unverified swim behaviors
**Status:** OPEN (filed 2026-07-30, Campaign P Slice P4 AP-10 closeout)
**Severity:** LOW (no confirmed divergence; research/verification follow-up)
**Component:** physics / terrain / water
**Context:** AP-10 (dry-corner water sink-in) is retired and `WATER_CONTACT_TS`
(`TransientStateFlags.WaterContact`) is now produced (mirrored alongside
`Contact` by `PhysicsObjUpdate.ApplySetPositionContact`,
`CommitSetPositionTransition`, and `PhysicsEngine`'s per-resolve body-state
commit). Three items from
`docs/research/2026-07-29-remote-and-world-specials-pseudocode.md` §5.3-5.4
remain genuinely unresolved — none block the AP-10 port, but none are
silently absorbed either:
1. **No confirmed retail CONSUMER of `WATER_CONTACT_TS` was found.** The
write site (`CPhysicsObj::SetPositionInternal`, pc:283459-283483) is
confirmed; a full xref scan for READS of bit 0x8 on `transient_state`
was not attempted (bitmask reads are not text-greppable across the
1.4M-line pseudo-C dump without high false-positive noise against
unrelated 0x8 masks). Is it a pure reporting/query bit (e.g. an "is
swimming" query for animation/sound/UI with no gameplay-feel
consequence), or does something in the movement/friction/step chain
branch on it? Next step: Ghidra MCP `/function_xrefs?name=CPhysicsObj::
SetPositionInternal` once reachable, or a live cdb capture.
2. **`CLandCell::find_env_collisions`'s ENTIRELY_WATER early-exit** (pc:317091:
`if (block_water_type == ENTIRELY_WATER && !ethereal && !(state&0x40)) return;`
— a swimming/ethereal exemption from terrain collision entirely) was
**not cross-checked** against acdream's handling of this exact condition.
Flagged as unverified, not asserted-divergent or asserted-matching.
3. **Jump-in-water and movement-effects-in-water** (reduced jump height, swim
animation triggers) were **not investigated** — out of the P4 physics/
collision scope; would need a `MovementSystem`/animation-side read.
**Files:** `src/AcDream.Core/Physics/PhysicsBody.cs` (`TransientStateFlags
.WaterContact`, `IsWaterContact`); `src/AcDream.Core/Physics/PhysicsObjUpdate.cs`;
`src/AcDream.Core/Physics/TransitionTypes.cs` (outdoor `FindEnvCollisions`
terrain branch — the ENTIRELY_WATER exemption's acdream-side home, if it
exists at all).
**Acceptance:** either (a) a confirmed consumer of `WATER_CONTACT_TS` is
found and ported (or confirmed absent, closing this cleanly), and (b) the
ENTIRELY_WATER early-exit is cross-checked and either confirmed matching or
filed as its own register row; or (c) this issue is re-scoped/split once one
sub-item resolves.
---
## #262 — Run-on-the-spot at first login: no displacement until a recall reset
**Status:** OPEN