From 9bd4d1eed8d9faf70f51de49de8cc8947ffb6acf Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 12 Apr 2026 09:58:07 +0200 Subject: [PATCH] =?UTF-8?q?feat(app):=20Phase=20B.3=20=E2=80=94=20wire=20P?= =?UTF-8?q?hysicsEngine=20into=20streaming=20pipeline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/AcDream.App/Rendering/GameWindow.cs | 64 ++++++++++++++++++- .../Streaming/StreamingController.cs | 6 +- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 02cf52c..c936c0c 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -34,6 +34,9 @@ public sealed class GameWindow : IDisposable private int _streamingRadius = 2; // default 5×5 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 // thread and the render thread both read dats (BuildLandblockForStreaming // on the worker; ApplyLoadedTerrain + live-spawn handlers on the render @@ -282,7 +285,12 @@ public sealed class GameWindow : IDisposable drainCompletions: _streamer.DrainCompletions, applyTerrain: ApplyLoadedTerrain, 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, // 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); } + // 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(); + var lbInfo = _dats.Get( + (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(envCellId); + if (envCell is null) continue; + if (envCell.EnvironmentId == 0) continue; + + var environment = _dats.Get( + 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( + 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; iterate Values. + var polyVids = new List>(cellStruct.PhysicsPolygons.Count); + foreach (var poly in cellStruct.PhysicsPolygons.Values) + { + var vids = new List(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. // EnsureUploaded is idempotent so duplicates across landblocks are free. if (_staticMesh is not null) diff --git a/src/AcDream.App/Streaming/StreamingController.cs b/src/AcDream.App/Streaming/StreamingController.cs index 6bdd957..67ed631 100644 --- a/src/AcDream.App/Streaming/StreamingController.cs +++ b/src/AcDream.App/Streaming/StreamingController.cs @@ -20,6 +20,7 @@ public sealed class StreamingController private readonly Action _enqueueUnload; private readonly Func> _drainCompletions; private readonly Action _applyTerrain; + private readonly Action? _removeTerrain; private readonly GpuWorldState _state; private StreamingRegion? _region; @@ -49,12 +50,14 @@ public sealed class StreamingController Func> drainCompletions, Action applyTerrain, GpuWorldState state, - int radius) + int radius, + Action? removeTerrain = null) { _enqueueLoad = enqueueLoad; _enqueueUnload = enqueueUnload; _drainCompletions = drainCompletions; _applyTerrain = applyTerrain; + _removeTerrain = removeTerrain; _state = state; Radius = radius; } @@ -94,6 +97,7 @@ public sealed class StreamingController break; case LandblockStreamResult.Unloaded unloaded: _state.RemoveLandblock(unloaded.LandblockId); + _removeTerrain?.Invoke(unloaded.LandblockId); break; case LandblockStreamResult.Failed failed: Console.WriteLine(