diff --git a/TombLib/TombLib.Test/DiagonalWallCollisionShapeTests.cs b/TombLib/TombLib.Test/DiagonalWallCollisionShapeTests.cs new file mode 100644 index 000000000..04029c3b9 --- /dev/null +++ b/TombLib/TombLib.Test/DiagonalWallCollisionShapeTests.cs @@ -0,0 +1,147 @@ +using System.Globalization; +using System.Reflection; +using TombLib.LevelData; +using TombLib.LevelData.Compilers; +using TombLib.LevelData.Compilers.TombEngine; +using TombLib.LevelData.SectorEnums; +using TombLib.LevelData.SectorStructs; + +namespace TombLib.Test; + +[TestClass] +public class DiagonalWallCollisionShapeTests +{ + private static readonly Type TombEngineRoomSectorShapeType = typeof(LevelCompilerTombEngine) + .GetNestedType("RoomSectorShape", BindingFlags.NonPublic)!; + private static readonly Type ClassicRoomSectorShapeType = typeof(LevelCompilerClassicTR) + .GetNestedType("RoomSectorShape", BindingFlags.NonPublic)!; + + [DataTestMethod] + [DataRow(DiagonalSplit.XnZn)] + [DataRow(DiagonalSplit.XnZp)] + [DataRow(DiagonalSplit.XpZn)] + [DataRow(DiagonalSplit.XpZp)] + public void TombEngineRoomSectorShape_FlattensDiagonalWallFloorCollision(DiagonalSplit diagonalSplit) + { + AssertDiagonalWallCollisionIsFlattened(TombEngineRoomSectorShapeType, diagonalSplit, isFloor: true); + } + + [DataTestMethod] + [DataRow(DiagonalSplit.XnZn)] + [DataRow(DiagonalSplit.XnZp)] + [DataRow(DiagonalSplit.XpZn)] + [DataRow(DiagonalSplit.XpZp)] + public void TombEngineRoomSectorShape_FlattensDiagonalWallCeilingCollision(DiagonalSplit diagonalSplit) + { + AssertDiagonalWallCollisionIsFlattened(TombEngineRoomSectorShapeType, diagonalSplit, isFloor: false); + } + + [TestMethod] + public void TombEngineRoomSectorShape_LeavesNonWallDiagonalSplitUntouched() + { + AssertNonWallDiagonalCollisionIsUntouched(TombEngineRoomSectorShapeType); + } + + [DataTestMethod] + [DataRow(DiagonalSplit.XnZn)] + [DataRow(DiagonalSplit.XnZp)] + [DataRow(DiagonalSplit.XpZn)] + [DataRow(DiagonalSplit.XpZp)] + public void ClassicRoomSectorShape_FlattensDiagonalWallFloorCollision(DiagonalSplit diagonalSplit) + { + AssertDiagonalWallCollisionIsFlattened(ClassicRoomSectorShapeType, diagonalSplit, isFloor: true); + } + + [DataTestMethod] + [DataRow(DiagonalSplit.XnZn)] + [DataRow(DiagonalSplit.XnZp)] + [DataRow(DiagonalSplit.XpZn)] + [DataRow(DiagonalSplit.XpZp)] + public void ClassicRoomSectorShape_FlattensDiagonalWallCeilingCollision(DiagonalSplit diagonalSplit) + { + AssertDiagonalWallCollisionIsFlattened(ClassicRoomSectorShapeType, diagonalSplit, isFloor: false); + } + + [TestMethod] + public void ClassicRoomSectorShape_LeavesNonWallDiagonalSplitUntouched() + { + AssertNonWallDiagonalCollisionIsUntouched(ClassicRoomSectorShapeType); + } + + private static Sector CreateDiagonalWallSector(DiagonalSplit diagonalSplit, bool isFloor) + { + var sector = new Sector(0, 0) + { + Type = SectorType.Wall + }; + + var surface = new SectorSurface + { + DiagonalSplit = diagonalSplit, + XnZn = 28, + XnZp = 4, + XpZn = 16, + XpZp = 40 + }; + + if (isFloor) + sector.Floor = surface; + else + sector.Ceiling = surface; + + return sector; + } + + private static (string FlatHeightField, string FirstFlattenedField, string SecondFlattenedField) GetFlatTriangleFields(DiagonalSplit diagonalSplit) + => diagonalSplit switch + { + DiagonalSplit.XnZn => ("HeightXpZp", "HeightXnZp", "HeightXpZn"), + DiagonalSplit.XnZp => ("HeightXpZn", "HeightXnZn", "HeightXpZp"), + DiagonalSplit.XpZn => ("HeightXnZp", "HeightXnZn", "HeightXpZp"), + DiagonalSplit.XpZp => ("HeightXnZn", "HeightXnZp", "HeightXpZn"), + _ => throw new ArgumentOutOfRangeException(nameof(diagonalSplit)) + }; + + private static void AssertDiagonalWallCollisionIsFlattened(Type roomSectorShapeType, DiagonalSplit diagonalSplit, bool isFloor) + { + var sector = CreateDiagonalWallSector(diagonalSplit, isFloor); + var shape = CreateRoomSectorShape(roomSectorShapeType, sector, isFloor); + var (flatHeightField, firstFlattenedField, secondFlattenedField) = GetFlatTriangleFields(diagonalSplit); + int flatHeight = GetField(roomSectorShapeType, shape, flatHeightField); + + Assert.AreEqual(flatHeight, GetField(roomSectorShapeType, shape, firstFlattenedField)); + Assert.AreEqual(flatHeight, GetField(roomSectorShapeType, shape, secondFlattenedField)); + Assert.AreEqual(0, GetField(roomSectorShapeType, shape, "DiagonalStep")); + } + + private static void AssertNonWallDiagonalCollisionIsUntouched(Type roomSectorShapeType) + { + var sector = new Sector(0, 0) + { + Type = SectorType.Floor, + Floor = new SectorSurface + { + DiagonalSplit = DiagonalSplit.XpZn, + XnZn = 28, + XnZp = 4, + XpZn = 16, + XpZp = 40 + } + }; + + var shape = CreateRoomSectorShape(roomSectorShapeType, sector, floor: true); + Assert.AreNotEqual(0, GetField(roomSectorShapeType, shape, "DiagonalStep")); + } + + private static object CreateRoomSectorShape(Type roomSectorShapeType, Sector sector, bool floor) + => Activator.CreateInstance(roomSectorShapeType, + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + binder: null, + args: new object[] { sector, floor, Room.RoomConnectionType.NoPortal, sector.IsAnyWall }, + culture: CultureInfo.InvariantCulture)!; + + private static T GetField(Type roomSectorShapeType, object instance, string fieldName) + => (T)roomSectorShapeType + .GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)! + .GetValue(instance)!; +} diff --git a/TombLib/TombLib/LevelData/Compilers/FloorData.cs b/TombLib/TombLib/LevelData/Compilers/FloorData.cs index 80d1dde45..ab306be5a 100644 --- a/TombLib/TombLib/LevelData/Compilers/FloorData.cs +++ b/TombLib/TombLib/LevelData/Compilers/FloorData.cs @@ -807,24 +807,51 @@ private struct RoomSectorShape public readonly int HeightXpZp; public readonly int DiagonalStep; + private static void FlattenDiagonalWallPart(DiagonalSplit diagonalSplit, ref int heightXnZn, ref int heightXnZp, ref int heightXpZn, ref int heightXpZp) + { + // Classic floordata uses the same diagonal wall collision hack as TEN: + // only the exposed flat triangle should contribute to QA / WS collision, + // while the hidden tilted half remains editor-side geometry data for now. + switch (diagonalSplit) + { + case DiagonalSplit.XnZn: + heightXnZp = heightXpZp; + heightXpZn = heightXpZp; + break; + case DiagonalSplit.XnZp: + heightXnZn = heightXpZn; + heightXpZp = heightXpZn; + break; + case DiagonalSplit.XpZn: + heightXnZn = heightXnZp; + heightXpZp = heightXnZp; + break; + case DiagonalSplit.XpZp: + heightXnZp = heightXnZn; + heightXpZn = heightXnZn; + break; + } + } + public RoomSectorShape(Sector sector, bool floor, Room.RoomConnectionType portalType, bool wall) { var surface = floor ? sector.Floor.WorldToClicks() : sector.Ceiling.WorldToClicks(); + int heightXnZn = surface.XnZn; + int heightXpZn = surface.XpZn; + int heightXnZp = surface.XnZp; + int heightXpZp = surface.XpZp; + int diagonalStep; - HeightXnZn = surface.XnZn; - HeightXpZn = surface.XpZn; - HeightXnZp = surface.XnZp; - HeightXpZp = surface.XpZp; SplitDirectionIsXEqualsZ = surface.SplitDirectionIsXEqualsZWithDiagonalSplit; if (sector.HasGhostBlock && sector.GhostBlock.Valid) { var ghostBlockSurface = floor ? sector.GhostBlock.Floor.WorldToClicks() : sector.GhostBlock.Ceiling.WorldToClicks(); - HeightXnZn += ghostBlockSurface.XnZn; - HeightXpZn += ghostBlockSurface.XpZn; - HeightXnZp += ghostBlockSurface.XnZp; - HeightXpZp += ghostBlockSurface.XpZp; + heightXnZn += ghostBlockSurface.XnZn; + heightXpZn += ghostBlockSurface.XpZn; + heightXnZp += ghostBlockSurface.XnZp; + heightXpZp += ghostBlockSurface.XpZp; } switch (portalType) @@ -864,35 +891,35 @@ public RoomSectorShape(Sector sector, bool floor, Room.RoomConnectionType portal switch (surface.DiagonalSplit) { case DiagonalSplit.None: - DiagonalStep = 0; + diagonalStep = 0; SplitWallFirst = wall; SplitWallSecond = wall; break; case DiagonalSplit.XnZn: - DiagonalStep = surface.XpZp - surface.XnZp; + diagonalStep = surface.XpZp - surface.XnZp; SplitWallFirst = wall; SplitWallSecond = false; break; case DiagonalSplit.XnZp: - DiagonalStep = surface.XpZn - surface.XpZp; + diagonalStep = surface.XpZn - surface.XpZp; SplitWallFirst = wall; SplitWallSecond = false; break; case DiagonalSplit.XpZn: - DiagonalStep = surface.XnZp - surface.XnZn; - HeightXnZn += DiagonalStep; - HeightXpZp += DiagonalStep; - DiagonalStep = -DiagonalStep; + diagonalStep = surface.XnZp - surface.XnZn; + heightXnZn += diagonalStep; + heightXpZp += diagonalStep; + diagonalStep = -diagonalStep; SplitWallFirst = false; SplitWallSecond = wall; break; case DiagonalSplit.XpZp: - DiagonalStep = surface.XnZn - surface.XpZn; - HeightXpZn += DiagonalStep; - HeightXnZp += DiagonalStep; - DiagonalStep = -DiagonalStep; + diagonalStep = surface.XnZn - surface.XpZn; + heightXpZn += diagonalStep; + heightXnZp += diagonalStep; + diagonalStep = -diagonalStep; SplitWallFirst = false; SplitWallSecond = wall; @@ -900,6 +927,18 @@ public RoomSectorShape(Sector sector, bool floor, Room.RoomConnectionType portal default: throw new ArgumentOutOfRangeException(); } + + if (wall && surface.DiagonalSplit != DiagonalSplit.None) + { + FlattenDiagonalWallPart(surface.DiagonalSplit, ref heightXnZn, ref heightXnZp, ref heightXpZn, ref heightXpZp); + diagonalStep = 0; + } + + HeightXnZn = heightXnZn; + HeightXpZn = heightXpZn; + HeightXnZp = heightXnZp; + HeightXpZp = heightXpZp; + DiagonalStep = diagonalStep; } public int Max => Math.Max(Math.Max(HeightXnZn, HeightXnZp), Math.Max(HeightXpZn, HeightXpZp)); diff --git a/TombLib/TombLib/LevelData/Compilers/TombEngine/FloorData.cs b/TombLib/TombLib/LevelData/Compilers/TombEngine/FloorData.cs index edd86306c..2272b45b5 100644 --- a/TombLib/TombLib/LevelData/Compilers/TombEngine/FloorData.cs +++ b/TombLib/TombLib/LevelData/Compilers/TombEngine/FloorData.cs @@ -388,22 +388,49 @@ private struct RoomSectorShape public readonly int HeightXpZp; public readonly int DiagonalStep; + private static void FlattenDiagonalWallPart(DiagonalSplit diagonalSplit, ref int heightXnZn, ref int heightXnZp, ref int heightXpZn, ref int heightXpZp) + { + // Diagonal walls only expose the flat triangle for QA / WS collision in TEN. + // The tilted surface on the hidden half is kept as editor-side geometry data as a hack + // until wall collision can use extra probes and represent both triangles independently. + switch (diagonalSplit) + { + case DiagonalSplit.XnZn: + heightXnZp = heightXpZp; + heightXpZn = heightXpZp; + break; + case DiagonalSplit.XnZp: + heightXnZn = heightXpZn; + heightXpZp = heightXpZn; + break; + case DiagonalSplit.XpZn: + heightXnZn = heightXnZp; + heightXpZp = heightXnZp; + break; + case DiagonalSplit.XpZp: + heightXnZp = heightXnZn; + heightXpZn = heightXnZn; + break; + } + } + public RoomSectorShape(Sector sector, bool floor, Room.RoomConnectionType portalType, bool wall) { var surface = floor ? sector.Floor : sector.Ceiling; + int heightXnZn = surface.XnZn; + int heightXpZn = surface.XpZn; + int heightXnZp = surface.XnZp; + int heightXpZp = surface.XpZp; + int diagonalStep; - HeightXnZn = surface.XnZn; - HeightXpZn = surface.XpZn; - HeightXnZp = surface.XnZp; - HeightXpZp = surface.XpZp; SplitDirectionIsXEqualsZ = surface.SplitDirectionIsXEqualsZWithDiagonalSplit; if (sector.HasGhostBlock && sector.GhostBlock.Valid) { - HeightXnZn += floor ? sector.GhostBlock.Floor.XnZn : sector.GhostBlock.Ceiling.XnZn; - HeightXpZn += floor ? sector.GhostBlock.Floor.XpZn : sector.GhostBlock.Ceiling.XpZn; - HeightXnZp += floor ? sector.GhostBlock.Floor.XnZp : sector.GhostBlock.Ceiling.XnZp; - HeightXpZp += floor ? sector.GhostBlock.Floor.XpZp : sector.GhostBlock.Ceiling.XpZp; + heightXnZn += floor ? sector.GhostBlock.Floor.XnZn : sector.GhostBlock.Ceiling.XnZn; + heightXpZn += floor ? sector.GhostBlock.Floor.XpZn : sector.GhostBlock.Ceiling.XpZn; + heightXnZp += floor ? sector.GhostBlock.Floor.XnZp : sector.GhostBlock.Ceiling.XnZp; + heightXpZp += floor ? sector.GhostBlock.Floor.XpZp : sector.GhostBlock.Ceiling.XpZp; } switch (portalType) @@ -443,35 +470,35 @@ public RoomSectorShape(Sector sector, bool floor, Room.RoomConnectionType portal switch (surface.DiagonalSplit) { case DiagonalSplit.None: - DiagonalStep = 0; + diagonalStep = 0; SplitWallFirst = wall; SplitWallSecond = wall; break; case DiagonalSplit.XnZn: - DiagonalStep = surface.XpZp - surface.XnZp; + diagonalStep = surface.XpZp - surface.XnZp; SplitWallFirst = wall; SplitWallSecond = false; break; case DiagonalSplit.XnZp: - DiagonalStep = surface.XpZn - surface.XpZp; + diagonalStep = surface.XpZn - surface.XpZp; SplitWallFirst = wall; SplitWallSecond = false; break; case DiagonalSplit.XpZn: - DiagonalStep = surface.XnZp - surface.XnZn; - HeightXnZn += DiagonalStep; - HeightXpZp += DiagonalStep; - DiagonalStep = -DiagonalStep; + diagonalStep = surface.XnZp - surface.XnZn; + heightXnZn += diagonalStep; + heightXpZp += diagonalStep; + diagonalStep = -diagonalStep; SplitWallFirst = false; SplitWallSecond = wall; break; case DiagonalSplit.XpZp: - DiagonalStep = surface.XnZn - surface.XpZn; - HeightXpZn += DiagonalStep; - HeightXnZp += DiagonalStep; - DiagonalStep = -DiagonalStep; + diagonalStep = surface.XnZn - surface.XpZn; + heightXpZn += diagonalStep; + heightXnZp += diagonalStep; + diagonalStep = -diagonalStep; SplitWallFirst = false; SplitWallSecond = wall; @@ -479,6 +506,18 @@ public RoomSectorShape(Sector sector, bool floor, Room.RoomConnectionType portal default: throw new ArgumentOutOfRangeException(); } + + if (wall && surface.DiagonalSplit != DiagonalSplit.None) + { + FlattenDiagonalWallPart(surface.DiagonalSplit, ref heightXnZn, ref heightXnZp, ref heightXpZn, ref heightXpZp); + diagonalStep = 0; + } + + HeightXnZn = heightXnZn; + HeightXpZn = heightXpZn; + HeightXnZp = heightXnZp; + HeightXpZp = heightXpZp; + DiagonalStep = diagonalStep; } public int Max => Math.Max(Math.Max(HeightXnZn, HeightXnZp), Math.Max(HeightXpZn, HeightXpZp));