fix(portal): synchronize destination presentation state
This commit is contained in:
parent
4b1bceefbb
commit
e95f55f25b
42 changed files with 2815 additions and 288 deletions
|
|
@ -66,15 +66,56 @@ public class PhysicsBodyCellSyncTests
|
|||
Assert.Equal(0u, body.CellPosition.ObjCellId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PositionDelta_InsideEnvCell_AdvancesCanonicalLocalFrame()
|
||||
{
|
||||
var body = new PhysicsBody();
|
||||
body.SnapToCell(
|
||||
0x8C0401ADu,
|
||||
worldPos: new Vector3(12f, -30f, 4f),
|
||||
cellLocal: new Vector3(80f, 40f, 4f));
|
||||
|
||||
body.Position += new Vector3(3f, -2f, 0.5f);
|
||||
|
||||
Assert.Equal(0x8C0401ADu, body.CellPosition.ObjCellId);
|
||||
Assert.Equal(new Vector3(83f, 38f, 4.5f), body.CellPosition.Frame.Origin);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CommitTransitionPosition_InsideDungeon_AdoptsResolvedEnvCellAndFrame()
|
||||
{
|
||||
var body = new PhysicsBody();
|
||||
body.SnapToCell(
|
||||
0x8C0401ADu,
|
||||
worldPos: new Vector3(12f, -30f, 4f),
|
||||
cellLocal: new Vector3(80f, 40f, 4f));
|
||||
|
||||
body.CommitTransitionPosition(
|
||||
0x8C0401AEu,
|
||||
new Vector3(17f, -27f, 4f));
|
||||
|
||||
Assert.Equal(0x8C0401AEu, body.CellPosition.ObjCellId);
|
||||
Assert.Equal(new Vector3(85f, 43f, 4f), body.CellPosition.Frame.Origin);
|
||||
Assert.Equal(new Vector3(17f, -27f, 4f), body.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CommitTransitionPosition_WithoutSeed_DoesNotInventCanonicalCell()
|
||||
{
|
||||
var body = new PhysicsBody();
|
||||
|
||||
body.CommitTransitionPosition(0x8C0401ADu, new Vector3(1f, 2f, 3f));
|
||||
|
||||
Assert.Equal(0u, body.CellPosition.ObjCellId);
|
||||
Assert.Equal(new Vector3(1f, 2f, 3f), body.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContinuousTracking_LongWalkAcrossCellsAndBlock_NeverGoesStale()
|
||||
{
|
||||
// Refutes the "CellPosition goes stale" concern (incl. the indoor-transit case):
|
||||
// because the Position setter re-derives the cell via AdjustToOutside on EVERY
|
||||
// write, CellPosition continuously tracks the outdoor cell under the body's WORLD
|
||||
// position. The body is always world-space, so the world delta is frame-invariant —
|
||||
// there is no separate "interior" frame to desync. On any exit it is the correct
|
||||
// cell, never stale.
|
||||
// Refutes outdoor CellPosition drift: the Position setter re-derives the
|
||||
// cell via AdjustToOutside on every write, so the carried (cell, local)
|
||||
// pair continuously tracks the body across cells and landblocks.
|
||||
var body = new PhysicsBody();
|
||||
body.SnapToCell(0xC95B0001u, new Vector3(1000f, 2000f, 12f), new Vector3(10f, 10f, 12f));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World.Cells;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics;
|
||||
|
|
@ -31,4 +32,123 @@ public class PhysicsEngineResidencyTests
|
|||
Assert.True(eng.IsLandblockTerrainResident(0xA9B40019u)); // cell-resolved id, same LB
|
||||
Assert.False(eng.IsLandblockTerrainResident(0xC6A90019u)); // different LB
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DemoteLandblockToTerrain_RemovesEnvCellsButPreservesTerrainResidency()
|
||||
{
|
||||
const uint landblockId = 0xA9B4FFFFu;
|
||||
var cache = new PhysicsDataCache();
|
||||
var engine = new PhysicsEngine { DataCache = cache };
|
||||
engine.AddLandblock(
|
||||
landblockId,
|
||||
new TerrainSurface(new byte[81], new float[256]),
|
||||
Array.Empty<CellSurface>(),
|
||||
Array.Empty<PortalPlane>(),
|
||||
0f,
|
||||
0f);
|
||||
cache.CellGraph.Add(new EnvCell(
|
||||
0xA9B40174u,
|
||||
Matrix4x4.Identity,
|
||||
Matrix4x4.Identity,
|
||||
Vector3.Zero,
|
||||
Vector3.One,
|
||||
Array.Empty<CellPortal>(),
|
||||
Array.Empty<uint>(),
|
||||
false,
|
||||
null));
|
||||
|
||||
engine.DemoteLandblockToTerrain(landblockId);
|
||||
|
||||
Assert.True(engine.IsLandblockTerrainResident(landblockId));
|
||||
Assert.Null(cache.CellGraph.GetVisible(0xA9B40174u));
|
||||
Assert.IsType<LandCell>(cache.CellGraph.GetVisible(0xA9B40001u));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DemoteLandblockToTerrain_DeregistersCrossBoundaryStaticButRetainsDynamic()
|
||||
{
|
||||
const uint landblockId = 0xA9B4FFFFu;
|
||||
const uint adjacentCell = 0xAAB40001u;
|
||||
const uint staticId = 100u;
|
||||
const uint dynamicId = 200u;
|
||||
var engine = new PhysicsEngine();
|
||||
engine.AddLandblock(
|
||||
landblockId,
|
||||
new TerrainSurface(new byte[81], new float[256]),
|
||||
Array.Empty<CellSurface>(),
|
||||
Array.Empty<PortalPlane>(),
|
||||
0f,
|
||||
0f);
|
||||
var seamPosition = new Vector3(191.5f, 12f, 1f);
|
||||
engine.ShadowObjects.Register(
|
||||
staticId,
|
||||
0x01000001u,
|
||||
seamPosition,
|
||||
Quaternion.Identity,
|
||||
2f,
|
||||
0f,
|
||||
0f,
|
||||
landblockId,
|
||||
isStatic: true);
|
||||
engine.ShadowObjects.Register(
|
||||
dynamicId,
|
||||
0x01000002u,
|
||||
seamPosition,
|
||||
Quaternion.Identity,
|
||||
2f,
|
||||
0f,
|
||||
0f,
|
||||
landblockId,
|
||||
isStatic: false);
|
||||
Assert.Contains(
|
||||
engine.ShadowObjects.GetObjectsInCell(adjacentCell),
|
||||
entry => entry.EntityId == staticId);
|
||||
Assert.Contains(
|
||||
engine.ShadowObjects.GetObjectsInCell(adjacentCell),
|
||||
entry => entry.EntityId == dynamicId);
|
||||
|
||||
engine.DemoteLandblockToTerrain(landblockId);
|
||||
|
||||
Assert.DoesNotContain(
|
||||
engine.ShadowObjects.AllEntriesForDebug(),
|
||||
entry => entry.EntityId == staticId);
|
||||
Assert.Contains(
|
||||
engine.ShadowObjects.GetObjectsInCell(adjacentCell),
|
||||
entry => entry.EntityId == dynamicId);
|
||||
Assert.Equal(1, engine.ShadowObjects.RetainedRegistrationCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveLandblock_DeregistersCrossBoundaryStaticButRetainsDynamic()
|
||||
{
|
||||
const uint landblockId = 0xA9B4FFFFu;
|
||||
const uint adjacentCell = 0xAAB40001u;
|
||||
const uint staticId = 300u;
|
||||
const uint dynamicId = 400u;
|
||||
var engine = new PhysicsEngine();
|
||||
engine.AddLandblock(
|
||||
landblockId,
|
||||
new TerrainSurface(new byte[81], new float[256]),
|
||||
Array.Empty<CellSurface>(),
|
||||
Array.Empty<PortalPlane>(),
|
||||
0f,
|
||||
0f);
|
||||
var seamPosition = new Vector3(191.5f, 12f, 1f);
|
||||
engine.ShadowObjects.Register(
|
||||
staticId, 0x01000003u, seamPosition, Quaternion.Identity, 2f,
|
||||
0f, 0f, landblockId, isStatic: true);
|
||||
engine.ShadowObjects.Register(
|
||||
dynamicId, 0x01000004u, seamPosition, Quaternion.Identity, 2f,
|
||||
0f, 0f, landblockId, isStatic: false);
|
||||
|
||||
engine.RemoveLandblock(landblockId);
|
||||
|
||||
Assert.DoesNotContain(
|
||||
engine.ShadowObjects.AllEntriesForDebug(),
|
||||
entry => entry.EntityId == staticId);
|
||||
Assert.Contains(
|
||||
engine.ShadowObjects.GetObjectsInCell(adjacentCell),
|
||||
entry => entry.EntityId == dynamicId);
|
||||
Assert.Equal(1, engine.ShadowObjects.RetainedRegistrationCount);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -207,6 +207,132 @@ public class ShadowObjectRegistryTests
|
|||
entry => entry.EntityId == entityId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RefloodLandblock_RestoresAdjacentOwnedFootprintWithdrawnByUnload()
|
||||
{
|
||||
const uint adjacentOwnerLb = 0xAAB40000u;
|
||||
const uint ownerCell = adjacentOwnerLb | 1u;
|
||||
const uint touchedCell = LbId | 57u; // west block cell (7,0)
|
||||
const uint entityId = 32u;
|
||||
var reg = new ShadowObjectRegistry();
|
||||
var cache = new PhysicsDataCache();
|
||||
cache.CellGraph.RegisterTerrain(
|
||||
adjacentOwnerLb,
|
||||
new TerrainSurface(new byte[81], new float[256]),
|
||||
new Vector3(192f, 0f, 0f));
|
||||
reg.DataCache = cache;
|
||||
reg.Register(
|
||||
entityId,
|
||||
0x01000006u,
|
||||
new Vector3(192.5f, 12f, 50f),
|
||||
Quaternion.Identity,
|
||||
2f,
|
||||
192f,
|
||||
0f,
|
||||
adjacentOwnerLb,
|
||||
seedCellId: ownerCell,
|
||||
isStatic: false);
|
||||
Assert.Contains(reg.GetObjectsInCell(touchedCell), e => e.EntityId == entityId);
|
||||
Assert.Contains(reg.GetObjectsInCell(ownerCell), e => e.EntityId == entityId);
|
||||
|
||||
reg.RemoveLandblock(LbId);
|
||||
Assert.DoesNotContain(reg.GetObjectsInCell(touchedCell), e => e.EntityId == entityId);
|
||||
Assert.Contains(reg.GetObjectsInCell(ownerCell), e => e.EntityId == entityId);
|
||||
Assert.Equal(1, reg.WithdrawnPrefixMarkerCount);
|
||||
|
||||
reg.RefloodLandblock(LbId);
|
||||
|
||||
Assert.Contains(reg.GetObjectsInCell(touchedCell), e => e.EntityId == entityId);
|
||||
Assert.Contains(reg.GetObjectsInCell(ownerCell), e => e.EntityId == entityId);
|
||||
Assert.Equal(0, reg.WithdrawnPrefixMarkerCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AuthoritativeMove_ClearsObsoleteWithdrawnPrefix()
|
||||
{
|
||||
const uint adjacentOwnerLb = 0xAAB40000u;
|
||||
const uint ownerCell = adjacentOwnerLb | 1u;
|
||||
const uint touchedCell = LbId | 57u;
|
||||
const uint entityId = 33u;
|
||||
var cache = new PhysicsDataCache();
|
||||
cache.CellGraph.RegisterTerrain(
|
||||
adjacentOwnerLb,
|
||||
new TerrainSurface(new byte[81], new float[256]),
|
||||
new Vector3(192f, 0f, 0f));
|
||||
var reg = new ShadowObjectRegistry { DataCache = cache };
|
||||
reg.Register(
|
||||
entityId, 0x01000007u, new Vector3(192.5f, 12f, 50f),
|
||||
Quaternion.Identity, 2f, 192f, 0f, adjacentOwnerLb,
|
||||
seedCellId: ownerCell, isStatic: false);
|
||||
reg.RemoveLandblock(LbId);
|
||||
Assert.Equal(1, reg.WithdrawnPrefixMarkerCount);
|
||||
|
||||
reg.UpdatePosition(
|
||||
entityId,
|
||||
new Vector3(204f, 12f, 50f),
|
||||
Quaternion.Identity,
|
||||
192f,
|
||||
0f,
|
||||
adjacentOwnerLb,
|
||||
seedCellId: ownerCell);
|
||||
|
||||
Assert.Equal(0, reg.WithdrawnPrefixMarkerCount);
|
||||
reg.RefloodLandblock(LbId);
|
||||
Assert.DoesNotContain(reg.GetObjectsInCell(touchedCell), e => e.EntityId == entityId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RefloodLandblock_RetiresOnlyCurrentMarker_WhenTwoPrefixesWereWithdrawn()
|
||||
{
|
||||
const uint ownerLb = 0xAAB50000u;
|
||||
const uint ownerCell = ownerLb | 1u;
|
||||
const uint westLb = 0xA9B50000u;
|
||||
const uint southLb = 0xAAB40000u;
|
||||
const uint westCell = westLb | 57u;
|
||||
const uint southCell = southLb | 8u;
|
||||
const uint entityId = 35u;
|
||||
var cache = new PhysicsDataCache();
|
||||
cache.CellGraph.RegisterTerrain(
|
||||
ownerLb,
|
||||
new TerrainSurface(new byte[81], new float[256]),
|
||||
new Vector3(192f, 192f, 0f));
|
||||
var reg = new ShadowObjectRegistry { DataCache = cache };
|
||||
reg.Register(
|
||||
entityId, 0x01000009u, new Vector3(192.5f, 192.5f, 50f),
|
||||
Quaternion.Identity, 2f, 192f, 192f, ownerLb,
|
||||
seedCellId: ownerCell, isStatic: false);
|
||||
Assert.Contains(reg.GetObjectsInCell(westCell), e => e.EntityId == entityId);
|
||||
Assert.Contains(reg.GetObjectsInCell(southCell), e => e.EntityId == entityId);
|
||||
|
||||
reg.RemoveLandblock(westLb);
|
||||
reg.RemoveLandblock(southLb);
|
||||
Assert.Equal(2, reg.WithdrawnPrefixMarkerCount);
|
||||
|
||||
reg.RefloodLandblock(westLb);
|
||||
Assert.Equal(1, reg.WithdrawnPrefixMarkerCount);
|
||||
|
||||
reg.RefloodLandblock(southLb);
|
||||
Assert.Equal(0, reg.WithdrawnPrefixMarkerCount);
|
||||
Assert.Contains(reg.GetObjectsInCell(westCell), e => e.EntityId == entityId);
|
||||
Assert.Contains(reg.GetObjectsInCell(southCell), e => e.EntityId == entityId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveLandblock_DirectStaticRetirement_ClearsWithdrawnMarker()
|
||||
{
|
||||
const uint entityId = 34u;
|
||||
var reg = new ShadowObjectRegistry();
|
||||
reg.Register(
|
||||
entityId, 0x01000008u, new Vector3(12f, 12f, 50f),
|
||||
Quaternion.Identity, 1f, OffX, OffY, LbId,
|
||||
isStatic: true);
|
||||
|
||||
reg.RemoveLandblock(LbId);
|
||||
|
||||
Assert.Equal(0, reg.RetainedRegistrationCount);
|
||||
Assert.Equal(0, reg.WithdrawnPrefixMarkerCount);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Per-cell query surface (BR-7 / A6.P4 2026-06-11): GetObjectsInCell IS
|
||||
// the query — retail CObjCell::find_obj_collisions iterates only the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue