diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index ef0087e..59d7dd1 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -198,6 +198,18 @@ public sealed class GameWindow : IDisposable if (_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var playerEntity)) { _playerController = new AcDream.App.Input.PlayerMovementController(_physicsEngine); + // Read the real step height from the player's Setup dat. + if (_dats is not null && (playerEntity.SourceGfxObjOrSetupId & 0xFF000000u) == 0x02000000u) + { + var playerSetup = _dats.Get(playerEntity.SourceGfxObjOrSetupId); + _playerController.StepUpHeight = (playerSetup is not null && playerSetup.StepUpHeight > 0f) + ? playerSetup.StepUpHeight + : 2f; // default human step height + } + else + { + _playerController.StepUpHeight = 2f; // default human step height + } // Derive initial cell ID from the entity's world position. int plbX = _liveCenterX + (int)MathF.Floor(playerEntity.Position.X / 192f); int plbY = _liveCenterY + (int)MathF.Floor(playerEntity.Position.Y / 192f); @@ -1323,6 +1335,7 @@ public sealed class GameWindow : IDisposable var terrainSurface = new AcDream.Core.Physics.TerrainSurface(lb.Heightmap.Height, _heightTable); var cellSurfaces = new List(); + var portalPlanes = new List(); var lbInfo = _dats.Get( (lb.LandblockId & 0xFFFF0000u) | 0xFFFEu); if (lbInfo is not null && lbInfo.NumCells > 0) @@ -1364,11 +1377,33 @@ public sealed class GameWindow : IDisposable } cellSurfaces.Add(new AcDream.Core.Physics.CellSurface(envCellId, worldVerts, polyVids)); + + // Extract portal planes from this EnvCell's CellPortals. + // CellPortal.PolygonId indexes cellStruct.Polygons (rendering polygons), + // NOT PhysicsPolygons — confirmed by ACViewer EnvCell.find_transit_cells. + foreach (var portal in envCell.CellPortals) + { + if (!cellStruct.Polygons.TryGetValue(portal.PolygonId, out var poly)) + continue; + if (poly.VertexIds.Count < 3) + continue; + + // VertexIds are short; worldVerts keys are ushort. + if (!worldVerts.TryGetValue((ushort)poly.VertexIds[0], out var v0)) continue; + if (!worldVerts.TryGetValue((ushort)poly.VertexIds[1], out var v1)) continue; + if (!worldVerts.TryGetValue((ushort)poly.VertexIds[2], out var v2)) continue; + + portalPlanes.Add(AcDream.Core.Physics.PortalPlane.FromVertices( + v0, v1, v2, + portal.OtherCellId, // target cell (0xFFFF = outdoor) + envCellId & 0xFFFFu, // owner cell (low 16 bits) + (ushort)portal.Flags)); + } } } _physicsEngine.AddLandblock(lb.LandblockId, terrainSurface, cellSurfaces, - Array.Empty(), origin.X, origin.Y); + portalPlanes, origin.X, origin.Y); } // Upload every GfxObj referenced by this landblock's entities. diff --git a/tests/AcDream.Core.Tests/World/SceneryGeneratorTests.cs b/tests/AcDream.Core.Tests/World/SceneryGeneratorTests.cs new file mode 100644 index 0000000..d003ea8 --- /dev/null +++ b/tests/AcDream.Core.Tests/World/SceneryGeneratorTests.cs @@ -0,0 +1,53 @@ +using AcDream.Core.World; +using DatReaderWriter.Types; + +namespace AcDream.Core.Tests.World; + +/// +/// Tests for SceneryGenerator road-exclusion logic. +/// The full Generate() pipeline requires real dat files (Region, Scene, etc.) +/// so road-check behavior is tested via the internal IsRoadVertex helper, +/// which is the single gate that guards against placing trees on roads. +/// +public class SceneryGeneratorTests +{ + // Terrain word layout (ushort): + // bits 0-1 = Road (non-zero → on a road) + // bits 2-6 = TerrainType + // bits 11-15 = SceneType + + [Theory] + [InlineData(0x0000, false)] // no road bits + [InlineData(0x0001, true)] // road bit 0 set + [InlineData(0x0002, true)] // road bit 1 set + [InlineData(0x0003, true)] // both road bits set + [InlineData(0x007C, false)] // terrain type bits only, no road + [InlineData(0xF800, false)] // scenery bits only, no road + [InlineData(0xF803, true)] // road + scenery bits + public void IsRoadVertex_CorrectlyIdentifiesRoadBits(ushort raw, bool expectedIsRoad) + { + Assert.Equal(expectedIsRoad, SceneryGenerator.IsRoadVertex(raw)); + } + + [Fact] + public void IsRoadVertex_ZeroTerrain_IsNotRoad() + { + // A fully blank terrain entry (no type, no road, no scene) is not a road. + Assert.False(SceneryGenerator.IsRoadVertex(0)); + } + + [Fact] + public void IsRoadVertex_MatchesTerrainInfoRoadProperty() + { + // Verify that IsRoadVertex agrees with the typed TerrainInfo.Road property + // for a sample of raw values, ensuring the bit convention is consistent. + for (ushort raw = 0; raw < 4; raw++) + { + TerrainInfo ti = raw; + bool expectedFromStruct = ti.Road != 0; + bool actual = SceneryGenerator.IsRoadVertex(raw); + Assert.True(actual == expectedFromStruct, + $"raw=0x{raw:X4}: IsRoadVertex={actual} but TerrainInfo.Road={ti.Road}"); + } + } +}