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:
parent
d6c3f8657a
commit
cc8d57a26e
7 changed files with 439 additions and 18 deletions
|
|
@ -517,22 +517,33 @@ public sealed class TerrainSurface
|
|||
/// character while dry terrain keeps feet exactly on the plane.
|
||||
///
|
||||
/// <para>
|
||||
/// AP-10 (Campaign P Slice P4, 2026-07-30): the retail 0.1 m dry-corner
|
||||
/// sink-in is RESTORED (was collapsed to 0 — see the retired register
|
||||
/// row's history). The collapse's justification ("0.1 destabilizes the
|
||||
/// feet-exactly-on-plane contact-touch check because <c>dist > EPSILON</c>
|
||||
/// skips <c>SetContactPlane</c> that tick") is structurally true of retail
|
||||
/// too — in ALL THREE implementations (retail, ACE, acdream) a skipped
|
||||
/// touch-reassertion is <b>not</b> a fall, because <see cref="ObjectInfoState.Contact"/>/
|
||||
/// <see cref="ObjectInfoState.OnWalkable"/> are STICKY: <c>PhysicsEngine
|
||||
/// .ResolveWithTransition</c>'s <c>onGround</c> computation ORs the fresh
|
||||
/// per-call <c>ContactPlaneValid</c> with the SEEDED (caller-supplied,
|
||||
/// itself sourced from <see cref="PhysicsBody.TransientState"/>'s persistent
|
||||
/// <see cref="TransientStateFlags.OnWalkable"/> bit) <c>ObjectInfoState
|
||||
/// .OnWalkable</c>, so a single skipped tick does not clear grounded state
|
||||
/// — the same sticky-bit model retail relies on. See
|
||||
/// <c>docs/research/2026-07-29-remote-and-world-specials-pseudocode.md</c>
|
||||
/// §5.2 for the full argument.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Ported from ACE <c>ObjCell.get_water_depth</c> and
|
||||
/// <c>LandblockStruct.calc_water_depth</c>, except that the retail
|
||||
/// 0.1 fallback for "dry corner of a partially-water cell" is
|
||||
/// collapsed to 0. The 0.1 offset destabilizes the "feet exactly on
|
||||
/// plane" contact-touch check (dist > EPSILON, so SetContactPlane
|
||||
/// doesn't fire, ValidateTransition clears OnWalkable, gravity
|
||||
/// applies, character floats/falls each frame). The visible effect
|
||||
/// in retail is subtle (~10 cm natural sink-in) so dropping it to 0
|
||||
/// is indistinguishable.
|
||||
/// <c>LandblockStruct.calc_water_depth</c>.
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item>NotWater cell → <c>0.0f</c></item>
|
||||
/// <item>EntirelyWater cell → <c>0.9f</c> (fully submerged)</item>
|
||||
/// <item>PartiallyWater cell, nearest-corner water type → <c>0.45f</c></item>
|
||||
/// <item>PartiallyWater cell, nearest-corner non-water → <c>0.0f</c>
|
||||
/// (retail uses 0.1, we use 0 to avoid the above-EPSILON dist bug)</item>
|
||||
/// <item>PartiallyWater cell, nearest-corner non-water → <c>0.1f</c>
|
||||
/// (retail's dry-corner sink-in)</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public float SampleWaterDepth(float localX, float localY)
|
||||
|
|
@ -548,14 +559,14 @@ public sealed class TerrainSurface
|
|||
|
||||
// PartiallyWater — resolve to nearest corner (retail picks the
|
||||
// terrain-vertex at the "+12" half of each axis, i.e. >= 12m into
|
||||
// a 24m cell rounds up). Return 0.45 for water corner, 0 for dry
|
||||
// (retail uses 0.1 for dry corners; see note above).
|
||||
// a 24m cell rounds up). 0.45 for a water corner, 0.1 for a dry
|
||||
// corner (retail's allowed dry-corner sink-in; see note above).
|
||||
float tx = fx - cx;
|
||||
float ty = fy - cy;
|
||||
int vx = cx + (tx >= 0.5f ? 1 : 0);
|
||||
int vy = cy + (ty >= 0.5f ? 1 : 0);
|
||||
|
||||
return _cornerIsWater[vx, vy] ? 0.45f : 0f;
|
||||
return _cornerIsWater[vx, vy] ? 0.45f : 0.1f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue