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

@ -99,10 +99,24 @@ public enum TransientStateFlags : uint
StationaryStop = 0x00000020, // bit 5 — fsf == 2
StationaryStuck = 0x00000040, // bit 6 — fsf == 3
Active = 0x00000080, // bit 7 — object needs per-frame update
// Declared to complete retail's TransientState (acclient.h:3688). Neither bit is
// produced or consumed by acdream's transition yet; they are here so the two free
// slots cannot be reused for something else and quietly collide with the wire.
/// <summary>
/// AP-10 (Campaign P Slice P4, 2026-07-30): retail <c>WATER_CONTACT_TS</c>
/// (acclient.h:3688). Retail writes it in <c>CPhysicsObj::SetPositionInternal</c>
/// (0x005153e5-0051545f) in the same statement block as <see cref="Contact"/>,
/// immediately after, from the transition's local <c>contact_plane_is_water</c>
/// (acdream: <see cref="PhysicsBody.ContactPlaneIsWater"/>). Produced (mirrored
/// alongside <see cref="Contact"/> by <c>PhysicsObjUpdate.ApplySetPositionContact</c>,
/// <c>PhysicsObjUpdate.CommitSetPositionTransition</c>, and
/// <c>PhysicsEngine</c>'s per-resolve body-state commit) since 2026-07-30; no
/// confirmed retail CONSUMER of the bit was found in this pass (a full
/// bit-0x8-read xref scan of the 1.4M-line pseudo-C dump was not attempted —
/// bitmask reads are not text-greppable without high false-positive noise
/// across unrelated 0x8 masks) — see the AP-10 register row.
/// </summary>
WaterContact = 0x00000008, // bit 3 — WATER_CONTACT_TS
// Declared to complete retail's TransientState (acclient.h:3688). Not
// produced or consumed by acdream's transition yet; here so the free
// slot cannot be reused for something else and quietly collide with the wire.
CheckEthereal = 0x00000100, // bit 8 — CHECK_ETHEREAL_TS
}
@ -431,6 +445,15 @@ public sealed class PhysicsBody
public bool OnWalkable => (TransientState & TransientStateFlags.OnWalkable) != 0;
public bool IsActive => (TransientState & TransientStateFlags.Active) != 0;
public bool InContact => (TransientState & TransientStateFlags.Contact) != 0;
/// <summary>
/// AP-10 (Campaign P Slice P4, 2026-07-30): retail <c>WATER_CONTACT_TS</c>
/// mirror of <see cref="ContactPlaneIsWater"/> onto <see cref="TransientState"/>.
/// Written in lockstep with <see cref="InContact"/> by
/// <see cref="PhysicsObjUpdate.ApplySetPositionContact"/>,
/// <see cref="PhysicsObjUpdate.CommitSetPositionTransition"/>, and
/// <see cref="PhysicsEngine"/>'s per-resolve body-state commit.
/// </summary>
public bool IsWaterContact => (TransientState & TransientStateFlags.WaterContact) != 0;
// ── FUN_00511420 ───────────────────────────────────────────────────────