fix(phys): #106 gate-2 — bogus-indoor-claim recovery + spawn-ground entry hold
Gate-2 fallout chain: session 1's bare-id wedge poisoned ACE's save (the client reported garbage cells while walking through walls), so session 2 logged in with (cell=0xA9B4013F inn interior, pos=(91.4,32.7)) — a position that cell does not contain. Probe evidence: exactly one [cell-transit] line all session; the player free-fell into an empty world. Two holes, both fixed at the root: 1. CellTransit pick escape hatch — restores the #83/A1.7 + #90 verification that lived in PhysicsEngine.ResolveCellId before the collide-then-pick rewrite moved membership into BuildCellSetAndPickContaining: an indoor current cell that IS hydrated but whose CellBSP no longer overlaps ANY part of the foot sphere is a bogus claim (corrupt save, or walked out through an unblocked gap). The portal BFS can never reach an exit portal from a cell the sphere isn't in, so no candidates exist and the claim held forever — wedging collision (ShadowObjectRegistry's #98 gate reads "indoor primary" -> outdoor object sweep skipped), wall BSP, terrain, and the render root. The pick now demotes to the outdoor column under the sphere centre (the LandDefs.AdjustToOutside result already computed for the pick — cross-block safe). Sphere-overlap (BSPQuery.SphereIntersectsCellBsp, pseudo_c:317666 -> :323267), NOT point-in: doorway push-back leaves the centre a few cm outside while the sphere still overlaps — no demotion, #90's ping-pong stays dead. An unhydrated cell cannot be verified — stale beats null while streaming hydrates (retail-equivalent: stale curr_cell kept when the pick finds nothing). 2. PlayerModeAutoEntry spawn-ground hold — player-mode entry now waits for the terrain under the spawn position to stream in (isSpawnGroundReady predicate, K.2 pattern). Entering earlier integrates gravity against an empty world: indoor-claimed spawns got no floor from any source and free-fell into the void; outdoor spawns raced hydration by ~1s every login. Retail never has this state (it loads cells synchronously) — the hold is the async-streaming equivalent of that invariant. With the hold, the entry snap (Resolve, stepUp=100) runs against hydrated cell floors + terrain and re-seats a corrupt save's claim immediately. Tests: IndoorSeed_SphereFullyOutsideHydratedCell_DemotesToOutdoorColumn (the gate-2 wedge shape, red pre-fix), straddle + no-BSP guards (the #90 hysteresis and stale-beats-null), TryEnter_Armed_SpawnGroundNotReady_ DoesNotFire. Full suite: 294+218+420 green; Core 1375 green + the same 4 pre-existing door/#99-era failures + 1 skip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
23adc9c9df
commit
6dbbf953c6
5 changed files with 184 additions and 5 deletions
|
|
@ -8,12 +8,13 @@ namespace AcDream.App.Input;
|
|||
/// <c>GameWindow</c> and ticked each frame from <c>OnUpdate</c>.
|
||||
///
|
||||
/// <para>
|
||||
/// Why is this its own class? The auto-entry has three independent
|
||||
/// Why is this its own class? The auto-entry has four independent
|
||||
/// preconditions (live session reaches <c>InWorld</c>, the player
|
||||
/// entity has been streamed into the world dictionary, and the player
|
||||
/// movement controller is constructible) plus a manual-override path
|
||||
/// entity has been streamed into the world dictionary, the player
|
||||
/// movement controller is constructible, and the terrain under the
|
||||
/// spawn position has streamed in) plus a manual-override path
|
||||
/// (the user can flip into fly mode before the auto-entry fires —
|
||||
/// their choice wins). All four interact with each other in a way
|
||||
/// their choice wins). All five interact with each other in a way
|
||||
/// that's painful to test through GameWindow but trivial here against
|
||||
/// fakes.
|
||||
/// </para>
|
||||
|
|
@ -43,6 +44,7 @@ public sealed class PlayerModeAutoEntry
|
|||
private readonly Func<bool> _isLiveInWorld;
|
||||
private readonly Func<bool> _isPlayerEntityPresent;
|
||||
private readonly Func<bool> _isPlayerControllerReady;
|
||||
private readonly Func<bool> _isSpawnGroundReady;
|
||||
private readonly Action _enterPlayerMode;
|
||||
|
||||
private bool _armed;
|
||||
|
|
@ -60,6 +62,12 @@ public sealed class PlayerModeAutoEntry
|
|||
/// PlayerMovementController is set up. Stays true once player mode
|
||||
/// is established; the auto-entry's job is to flip it from false
|
||||
/// to true exactly once.</param>
|
||||
/// <param name="isSpawnGroundReady">True iff the terrain under the
|
||||
/// player's spawn position has streamed into the physics engine.
|
||||
/// #106 gate-2 (2026-06-09): entering player mode earlier integrates
|
||||
/// gravity against an empty world and free-falls the player into the
|
||||
/// void. Retail never has this state — it loads cells synchronously;
|
||||
/// this hold is the async-streaming equivalent of that invariant.</param>
|
||||
/// <param name="enterPlayerMode">Action invoked on the firing
|
||||
/// tick. The same routine the manual Tab handler invokes (fly →
|
||||
/// player transition). Must construct the controller + chase
|
||||
|
|
@ -69,11 +77,13 @@ public sealed class PlayerModeAutoEntry
|
|||
Func<bool> isLiveInWorld,
|
||||
Func<bool> isPlayerEntityPresent,
|
||||
Func<bool> isPlayerControllerReady,
|
||||
Func<bool> isSpawnGroundReady,
|
||||
Action enterPlayerMode)
|
||||
{
|
||||
_isLiveInWorld = isLiveInWorld ?? throw new ArgumentNullException(nameof(isLiveInWorld));
|
||||
_isPlayerEntityPresent = isPlayerEntityPresent ?? throw new ArgumentNullException(nameof(isPlayerEntityPresent));
|
||||
_isPlayerControllerReady = isPlayerControllerReady ?? throw new ArgumentNullException(nameof(isPlayerControllerReady));
|
||||
_isSpawnGroundReady = isSpawnGroundReady ?? throw new ArgumentNullException(nameof(isSpawnGroundReady));
|
||||
_enterPlayerMode = enterPlayerMode ?? throw new ArgumentNullException(nameof(enterPlayerMode));
|
||||
}
|
||||
|
||||
|
|
@ -107,6 +117,7 @@ public sealed class PlayerModeAutoEntry
|
|||
if (!_isLiveInWorld()) return false;
|
||||
if (!_isPlayerEntityPresent()) return false;
|
||||
if (!_isPlayerControllerReady()) return false;
|
||||
if (!_isSpawnGroundReady()) return false;
|
||||
|
||||
_armed = false;
|
||||
_enterPlayerMode();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue