fix(physics): #31 update outdoor cell id during transition movement
This commit is contained in:
parent
3be0c8b7c7
commit
9fea9b13ad
5 changed files with 109 additions and 29 deletions
|
|
@ -177,33 +177,6 @@ missing is the plugin-API surface.
|
|||
|
||||
---
|
||||
|
||||
## #31 — Low outdoor cell id can go stale after transition movement
|
||||
|
||||
**Status:** OPEN
|
||||
**Severity:** HIGH
|
||||
**Filed:** 2026-04-29
|
||||
**Component:** physics / cells / movement
|
||||
|
||||
**Description:** Local movement can cross 24m outdoor cell boundaries while
|
||||
the low cell id used for outbound full cell id remains stale. This can combine
|
||||
correct landblock high bits with the wrong outdoor-cell low byte.
|
||||
|
||||
**Root cause / status:** Tracked under Phase L.2e. `CELLARRAY`,
|
||||
`CObjCell::find_cell_list`, adjacent-cell checks, and low-cell ownership are
|
||||
not fully ported.
|
||||
|
||||
**Files:** `src/AcDream.Core/Physics/PhysicsEngine.cs`,
|
||||
`src/AcDream.Core/Physics/TransitionTypes.cs`,
|
||||
`src/AcDream.App/Input/PlayerMovementController.cs`.
|
||||
|
||||
**Research:** `docs/plans/2026-04-29-movement-collision-conformance.md`.
|
||||
|
||||
**Acceptance:** Crossing a 24m outdoor-cell seam updates the local resolved
|
||||
cell id and the outbound full cell id. Tests cover intra-landblock seams and
|
||||
landblock-edge seams.
|
||||
|
||||
---
|
||||
|
||||
## #32 — Retail edge-slide / cliff-slide / precipice-slide incomplete
|
||||
|
||||
**Status:** OPEN
|
||||
|
|
@ -440,6 +413,18 @@ If hypothesis (a) is correct, this issue effectively rolls into **#28** — the
|
|||
|
||||
# Recently closed
|
||||
|
||||
## #31 — [DONE 2026-04-29] Low outdoor cell id can go stale after transition movement
|
||||
|
||||
**Closed:** 2026-04-29
|
||||
**Commit:** `(this commit)`
|
||||
**Resolution:** `ResolveWithTransition` now refreshes outdoor cell ownership
|
||||
from the resolved world position while the sphere sweep runs. Intra-landblock
|
||||
24m outdoor seams update the low cell id, and full-cell callers crossing a
|
||||
landblock seam get the destination landblock prefix plus the correct outdoor
|
||||
low cell.
|
||||
|
||||
---
|
||||
|
||||
## #34 — [DONE 2026-04-29] Missing routine local/server correction diagnostic
|
||||
|
||||
**Closed:** 2026-04-29
|
||||
|
|
|
|||
|
|
@ -55,3 +55,7 @@ InputDispatcher / PlayerMovementController
|
|||
`move-truth ECHO` for player `UpdatePosition` echoes, including local/server
|
||||
delta. `GameWindow` now passes explicit grounded/airborne contact bytes from
|
||||
`MovementResult.IsOnGround` to both movement packet builders.
|
||||
- 2026-04-29: L.2e first cell-ownership fix. `ResolveWithTransition` refreshes
|
||||
outdoor cell ownership from world position during the sphere sweep, so 24m
|
||||
outdoor seams update low cell ids and full-cell callers crossing landblock
|
||||
seams get the destination landblock prefix plus the correct outdoor low cell.
|
||||
|
|
|
|||
|
|
@ -162,6 +162,38 @@ public sealed class PhysicsEngine
|
|||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the outdoor cell id that owns a world-space position.
|
||||
/// Indoor ids are preserved because EnvCell ownership still comes from
|
||||
/// portal/cell BSP state; outdoor ids are derived from the registered
|
||||
/// landblock that currently contains the point.
|
||||
/// </summary>
|
||||
internal uint ResolveOutdoorCellId(Vector3 worldPos, uint fallbackCellId)
|
||||
{
|
||||
if (fallbackCellId == 0)
|
||||
return 0;
|
||||
|
||||
uint fallbackLow = fallbackCellId & 0xFFFFu;
|
||||
if (fallbackLow >= 0x0100u)
|
||||
return fallbackCellId;
|
||||
|
||||
foreach (var kvp in _landblocks)
|
||||
{
|
||||
var lb = kvp.Value;
|
||||
float localX = worldPos.X - lb.WorldOffsetX;
|
||||
float localY = worldPos.Y - lb.WorldOffsetY;
|
||||
if (localX >= 0f && localX < 192f && localY >= 0f && localY < 192f)
|
||||
{
|
||||
uint lowCellId = lb.Terrain.ComputeOutdoorCellId(localX, localY);
|
||||
return (fallbackCellId & 0xFFFF0000u) == 0
|
||||
? lowCellId
|
||||
: (kvp.Key & 0xFFFF0000u) | lowCellId;
|
||||
}
|
||||
}
|
||||
|
||||
return fallbackCellId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve an entity's movement from <paramref name="currentPos"/> by
|
||||
/// applying <paramref name="delta"/> (XY only) and computing the correct Z
|
||||
|
|
@ -471,7 +503,10 @@ public sealed class PhysicsEngine
|
|||
bool onGround = ci.ContactPlaneValid
|
||||
|| transition.ObjectInfo.State.HasFlag(ObjectInfoState.OnWalkable);
|
||||
|
||||
return new ResolveResult(sp.CheckPos, sp.CheckCellId, onGround);
|
||||
return new ResolveResult(
|
||||
sp.CheckPos,
|
||||
ResolveOutdoorCellId(sp.CheckPos, sp.CheckCellId),
|
||||
onGround);
|
||||
}
|
||||
|
||||
// Transition failed (e.g., stuck in corner, too many steps).
|
||||
|
|
@ -483,6 +518,10 @@ public sealed class PhysicsEngine
|
|||
|| transition.ObjectInfo.State.HasFlag(ObjectInfoState.OnWalkable)
|
||||
|| isOnGround;
|
||||
|
||||
return new ResolveResult(sp.CheckPos, sp.CheckCellId != 0 ? sp.CheckCellId : cellId, partialOnGround);
|
||||
uint partialCellId = sp.CheckCellId != 0 ? sp.CheckCellId : cellId;
|
||||
return new ResolveResult(
|
||||
sp.CheckPos,
|
||||
ResolveOutdoorCellId(sp.CheckPos, partialCellId),
|
||||
partialOnGround);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -705,6 +705,10 @@ public sealed class Transition
|
|||
var sp = SpherePath;
|
||||
var ci = CollisionInfo;
|
||||
|
||||
uint resolvedOutdoorCellId = engine.ResolveOutdoorCellId(sp.CheckPos, sp.CheckCellId);
|
||||
if (resolvedOutdoorCellId != sp.CheckCellId)
|
||||
sp.SetCheckPos(sp.CheckPos, resolvedOutdoorCellId);
|
||||
|
||||
Vector3 footCenter = sp.GlobalSphere[0].Origin;
|
||||
float sphereRadius = sp.GlobalSphere[0].Radius;
|
||||
|
||||
|
|
|
|||
|
|
@ -190,6 +190,54 @@ public class PhysicsEngineTests
|
|||
Assert.True(result.Position.X > 192f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveWithTransition_OutdoorCellBoundary_UpdatesLowCellId()
|
||||
{
|
||||
var engine = MakeFlatEngine(terrainZ: 50f);
|
||||
|
||||
var result = engine.ResolveWithTransition(
|
||||
currentPos: new Vector3(23f, 10f, 50f),
|
||||
targetPos: new Vector3(25f, 10f, 50f),
|
||||
cellId: 0x0001u,
|
||||
sphereRadius: 0.5f,
|
||||
sphereHeight: 1.2f,
|
||||
stepUpHeight: 0.4f,
|
||||
stepDownHeight: 0.4f,
|
||||
isOnGround: true);
|
||||
|
||||
Assert.True(result.IsOnGround);
|
||||
Assert.InRange(result.Position.X, 24.9f, 25.1f);
|
||||
Assert.Equal(0x0009u, result.CellId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveWithTransition_LandblockBoundary_UpdatesFullOutdoorCellId()
|
||||
{
|
||||
var engine = new PhysicsEngine();
|
||||
|
||||
var terrainA = new TerrainSurface(FlatHeightmap(50), LinearHeightTable());
|
||||
engine.AddLandblock(0xA9B4FFFFu, terrainA, Array.Empty<CellSurface>(),
|
||||
Array.Empty<PortalPlane>(), worldOffsetX: 0f, worldOffsetY: 0f);
|
||||
|
||||
var terrainB = new TerrainSurface(FlatHeightmap(50), LinearHeightTable());
|
||||
engine.AddLandblock(0xAAB4FFFFu, terrainB, Array.Empty<CellSurface>(),
|
||||
Array.Empty<PortalPlane>(), worldOffsetX: 192f, worldOffsetY: 0f);
|
||||
|
||||
var result = engine.ResolveWithTransition(
|
||||
currentPos: new Vector3(191f, 10f, 50f),
|
||||
targetPos: new Vector3(193f, 10f, 50f),
|
||||
cellId: 0xA9B40039u,
|
||||
sphereRadius: 0.5f,
|
||||
sphereHeight: 1.2f,
|
||||
stepUpHeight: 0.4f,
|
||||
stepDownHeight: 0.4f,
|
||||
isOnGround: true);
|
||||
|
||||
Assert.True(result.IsOnGround);
|
||||
Assert.InRange(result.Position.X, 192.9f, 193.1f);
|
||||
Assert.Equal(0xAAB40001u, result.CellId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolve_LeaveIndoorCell_TransitionsToOutdoor()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue