feat(phys): A6.P3 slice 1 step 1 — add Mechanism B (LKCP restore)

Restores CollisionInfo.ContactPlane from LastKnownContactPlane when:
  - LKCP is valid
  - the sphere's current center is geometrically close to the LKCP
    plane (|dot(global_curr_center, N) + d| <= radius + EPSILON)

Matches retail's validate_transition LKCP-restore at
acclient_2013_pseudo_c.txt:272577 (CTransition::validate_transition,
address 0050aa70, lines 272565-272582). Slice 1 step 1 of the
A6.P3 indoor CP retention fix. Step 2 (Task 5) strips the
TryFindIndoorWalkablePlane synthesis from FindEnvCollisions.

Also fixes the proximity-check sphere: was using
sp.GlobalSphere[0].Origin (start sphere); now uses
sp.GlobalCurrCenter[0].Origin (current center) per retail
(acclient_2013_pseudo_c.txt:272568).

Tests: 1147 pass, 9 fail (8 pre-existing + 1 IndoorContactPlaneRetention
from T3 — expected; T5 lands the actual synthesis-strip fix).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-22 08:58:03 +02:00
parent a32f56955d
commit 5aba071aec

View file

@ -2849,13 +2849,38 @@ public sealed class Transition
// //
// Matches ACE PhysicsObj's pre-reuse check on the last-known // Matches ACE PhysicsObj's pre-reuse check on the last-known
// plane and retail's CPhysicsObj::get_object_info logic. // plane and retail's CPhysicsObj::get_object_info logic.
var sphereCenter = sp.GlobalSphere[0].Origin; // A6.P3 slice 1 (2026-05-21). Retail uses global_curr_center (NOT
// global_sphere->center) for this proximity check — see
// acclient_2013_pseudo_c.txt:272568. global_sphere is the START
// sphere of the transition; global_curr_center is the CURRENT center
// after sub-step accumulation. Using the wrong one made the proximity
// guard fire on the wrong reference point.
var sphereCenter = sp.GlobalCurrCenter[0].Origin;
var radius = sp.GlobalSphere[0].Radius; var radius = sp.GlobalSphere[0].Radius;
float angle = Vector3.Dot(ci.LastKnownContactPlane.Normal, sphereCenter) float angle = Vector3.Dot(ci.LastKnownContactPlane.Normal, sphereCenter)
+ ci.LastKnownContactPlane.D; + ci.LastKnownContactPlane.D;
if (radius + PhysicsGlobals.EPSILON > MathF.Abs(angle)) if (radius + PhysicsGlobals.EPSILON > MathF.Abs(angle))
{ {
// ── Mechanism B — restore CP from LKCP per retail ────────────────
// A6.P3 slice 1 (2026-05-21). Retail oracle:
// acclient_2013_pseudo_c.txt:272577 (inside CTransition::validate_transition
// at line 272547). When the sphere is geometrically close to the
// LastKnownContactPlane, retail restores CP from LKCP via
// set_contact_plane(&collision_info, &last_known_contact_plane,
// last_known_contact_plane_is_water). This closes the gap that the
// stripped TryFindIndoorWalkablePlane synthesis path used to fill —
// when no fresh Path-6 CP write lands in this transition, CP is
// retained from the previous frame instead of being re-synthesized.
//
// NOTE: SetContactPlane also re-latches LKCP fields
// (TransitionTypes.cs:258-261), which is a no-op here since we
// pass LKCP as the source.
ci.SetContactPlane(
ci.LastKnownContactPlane,
ci.LastKnownContactPlaneCellId,
ci.LastKnownContactPlaneIsWater);
// Still close enough to the last-known plane — preserve // Still close enough to the last-known plane — preserve
// grounded state. L.2.3i FloorZ test for OnWalkable. // grounded state. L.2.3i FloorZ test for OnWalkable.
oi.State |= ObjectInfoState.Contact; oi.State |= ObjectInfoState.Contact;