feat(app): Phase B.3 — wire PhysicsEngine into streaming pipeline

Populates the collision engine with TerrainSurface + CellSurface
entries when landblocks stream in, removes them when they stream
out. CellSurface vertices are transformed from cell-local to world
space using EnvCell.Position orientation + origin.

Phase B.2 (player movement mode) will call PhysicsEngine.Resolve()
to get collision-validated positions before sending them to the
server.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-12 09:58:07 +02:00
parent 88d446d11d
commit 9bd4d1eed8
2 changed files with 68 additions and 2 deletions

View file

@ -34,6 +34,9 @@ public sealed class GameWindow : IDisposable
private int _streamingRadius = 2; // default 5×5 private int _streamingRadius = 2; // default 5×5
private uint? _lastLivePlayerLandblockId; private uint? _lastLivePlayerLandblockId;
// Phase B.3: physics engine — populated from the streaming pipeline.
private readonly AcDream.Core.Physics.PhysicsEngine _physicsEngine = new();
// Phase A.1 hotfix: DatCollection is NOT thread-safe. The streaming worker // Phase A.1 hotfix: DatCollection is NOT thread-safe. The streaming worker
// thread and the render thread both read dats (BuildLandblockForStreaming // thread and the render thread both read dats (BuildLandblockForStreaming
// on the worker; ApplyLoadedTerrain + live-spawn handlers on the render // on the worker; ApplyLoadedTerrain + live-spawn handlers on the render
@ -282,7 +285,12 @@ public sealed class GameWindow : IDisposable
drainCompletions: _streamer.DrainCompletions, drainCompletions: _streamer.DrainCompletions,
applyTerrain: ApplyLoadedTerrain, applyTerrain: ApplyLoadedTerrain,
state: _worldState, state: _worldState,
radius: _streamingRadius); radius: _streamingRadius,
removeTerrain: id =>
{
_terrain?.RemoveLandblock(id);
_physicsEngine.RemoveLandblock(id);
});
// Phase 4.7: optional live-mode startup. Connect to the ACE server, // Phase 4.7: optional live-mode startup. Connect to the ACE server,
// enter the world as the first character on the account, and stream // enter the world as the first character on the account, and stream
@ -1211,6 +1219,60 @@ public sealed class GameWindow : IDisposable
_worldState.SetLandblockAabb(lb.LandblockId, aabbMin, aabbMax); _worldState.SetLandblockAabb(lb.LandblockId, aabbMin, aabbMax);
} }
// Phase B.3: populate the physics engine with terrain + indoor cell
// surfaces for this landblock. Runs under _datLock (same lock as the
// rest of ApplyLoadedTerrainLocked) so dat reads are safe.
{
var terrainSurface = new AcDream.Core.Physics.TerrainSurface(lb.Heightmap.Height, _heightTable);
var cellSurfaces = new List<AcDream.Core.Physics.CellSurface>();
var lbInfo = _dats.Get<DatReaderWriter.DBObjs.LandBlockInfo>(
(lb.LandblockId & 0xFFFF0000u) | 0xFFFEu);
if (lbInfo is not null && lbInfo.NumCells > 0)
{
uint firstCellId = (lb.LandblockId & 0xFFFF0000u) | 0x0100u;
for (uint offset = 0; offset < lbInfo.NumCells; offset++)
{
uint envCellId = firstCellId + offset;
var envCell = _dats.Get<DatReaderWriter.DBObjs.EnvCell>(envCellId);
if (envCell is null) continue;
if (envCell.EnvironmentId == 0) continue;
var environment = _dats.Get<DatReaderWriter.DBObjs.Environment>(
0x0D000000u | envCell.EnvironmentId);
if (environment is null) continue;
if (!environment.Cells.TryGetValue(envCell.CellStructure, out var cellStruct)) continue;
// Transform CellStruct vertices from cell-local to world space.
var rot = envCell.Position.Orientation;
var cellOriginWorld = envCell.Position.Origin + origin;
var worldVerts = new Dictionary<ushort, System.Numerics.Vector3>(
cellStruct.VertexArray.Vertices.Count);
foreach (var (vid, vtx) in cellStruct.VertexArray.Vertices)
{
var localPos = vtx.Origin;
var worldPos = System.Numerics.Vector3.Transform(localPos, rot) + cellOriginWorld;
worldVerts[(ushort)vid] = worldPos;
}
// Extract polygon vertex-id lists from PhysicsPolygons.
// PhysicsPolygons is Dictionary<ushort, Polygon>; iterate Values.
var polyVids = new List<List<short>>(cellStruct.PhysicsPolygons.Count);
foreach (var poly in cellStruct.PhysicsPolygons.Values)
{
var vids = new List<short>(poly.VertexIds.Count);
foreach (var vid in poly.VertexIds)
vids.Add(vid);
polyVids.Add(vids);
}
cellSurfaces.Add(new AcDream.Core.Physics.CellSurface(envCellId, worldVerts, polyVids));
}
}
_physicsEngine.AddLandblock(lb.LandblockId, terrainSurface, cellSurfaces, origin.X, origin.Y);
}
// Upload every GfxObj referenced by this landblock's entities. // Upload every GfxObj referenced by this landblock's entities.
// EnsureUploaded is idempotent so duplicates across landblocks are free. // EnsureUploaded is idempotent so duplicates across landblocks are free.
if (_staticMesh is not null) if (_staticMesh is not null)

View file

@ -20,6 +20,7 @@ public sealed class StreamingController
private readonly Action<uint> _enqueueUnload; private readonly Action<uint> _enqueueUnload;
private readonly Func<int, IReadOnlyList<LandblockStreamResult>> _drainCompletions; private readonly Func<int, IReadOnlyList<LandblockStreamResult>> _drainCompletions;
private readonly Action<LoadedLandblock> _applyTerrain; private readonly Action<LoadedLandblock> _applyTerrain;
private readonly Action<uint>? _removeTerrain;
private readonly GpuWorldState _state; private readonly GpuWorldState _state;
private StreamingRegion? _region; private StreamingRegion? _region;
@ -49,12 +50,14 @@ public sealed class StreamingController
Func<int, IReadOnlyList<LandblockStreamResult>> drainCompletions, Func<int, IReadOnlyList<LandblockStreamResult>> drainCompletions,
Action<LoadedLandblock> applyTerrain, Action<LoadedLandblock> applyTerrain,
GpuWorldState state, GpuWorldState state,
int radius) int radius,
Action<uint>? removeTerrain = null)
{ {
_enqueueLoad = enqueueLoad; _enqueueLoad = enqueueLoad;
_enqueueUnload = enqueueUnload; _enqueueUnload = enqueueUnload;
_drainCompletions = drainCompletions; _drainCompletions = drainCompletions;
_applyTerrain = applyTerrain; _applyTerrain = applyTerrain;
_removeTerrain = removeTerrain;
_state = state; _state = state;
Radius = radius; Radius = radius;
} }
@ -94,6 +97,7 @@ public sealed class StreamingController
break; break;
case LandblockStreamResult.Unloaded unloaded: case LandblockStreamResult.Unloaded unloaded:
_state.RemoveLandblock(unloaded.LandblockId); _state.RemoveLandblock(unloaded.LandblockId);
_removeTerrain?.Invoke(unloaded.LandblockId);
break; break;
case LandblockStreamResult.Failed failed: case LandblockStreamResult.Failed failed:
Console.WriteLine( Console.WriteLine(