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
|
|
@ -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 ───────────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -1298,6 +1298,17 @@ public sealed class PhysicsEngine
|
|||
body.ContactPlaneValid = false;
|
||||
}
|
||||
|
||||
// AP-10 (Campaign P Slice P4, 2026-07-30): retail SetPositionInternal
|
||||
// (0x005153e5-0051545f) writes WATER_CONTACT_TS in the same statement
|
||||
// block as CONTACT_TS, immediately after — this is acdream's equivalent
|
||||
// per-resolve commit point. Mirrors whatever body.ContactPlaneIsWater was
|
||||
// just set to above (unchanged/stale in the no-valid-contact branch,
|
||||
// matching that branch's existing ContactPlaneIsWater behavior).
|
||||
if (body.ContactPlaneIsWater)
|
||||
body.TransientState |= TransientStateFlags.WaterContact;
|
||||
else
|
||||
body.TransientState &= ~TransientStateFlags.WaterContact;
|
||||
|
||||
// Publish frames_stationary_fall + carry it to the next frame via the Stationary*
|
||||
// transient bits. Retail encodes these bits in handle_all_collisions (pc:282737-758);
|
||||
// acdream co-locates the encode with the fsf writeback here (STRUCTURAL ADAPTATION,
|
||||
|
|
|
|||
|
|
@ -42,6 +42,16 @@ public static class PhysicsObjUpdate
|
|||
else
|
||||
body.TransientState &= ~TransientStateFlags.OnWalkable;
|
||||
|
||||
// AP-10 (Campaign P Slice P4, 2026-07-30): retail SetPositionInternal
|
||||
// (0x005153e5-0051545f) writes WATER_CONTACT_TS in the same statement
|
||||
// block as CONTACT_TS, immediately after. body.ContactPlaneIsWater is
|
||||
// already current by the time this runs (every caller sets it, or it
|
||||
// carries over from the prior tick, before calling here).
|
||||
if (body.ContactPlaneIsWater)
|
||||
body.TransientState |= TransientStateFlags.WaterContact;
|
||||
else
|
||||
body.TransientState &= ~TransientStateFlags.WaterContact;
|
||||
|
||||
body.calc_acceleration();
|
||||
}
|
||||
|
||||
|
|
@ -96,6 +106,15 @@ public static class PhysicsObjUpdate
|
|||
else
|
||||
body.TransientState &= ~TransientStateFlags.OnWalkable;
|
||||
|
||||
// AP-10 (Campaign P Slice P4, 2026-07-30): mirror WATER_CONTACT_TS
|
||||
// alongside CONTACT_TS/ON_WALKABLE_TS, same as ApplySetPositionContact.
|
||||
// Callers (e.g. RemoteTeleportPlacement) already set body.ContactPlaneIsWater
|
||||
// before invoking this commit.
|
||||
if (body.ContactPlaneIsWater)
|
||||
body.TransientState |= TransientStateFlags.WaterContact;
|
||||
else
|
||||
body.TransientState &= ~TransientStateFlags.WaterContact;
|
||||
|
||||
if (!previousOnWalkable && finalOnWalkable)
|
||||
{
|
||||
hitGround?.Invoke();
|
||||
|
|
|
|||
|
|
@ -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