Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions TombLib/TombLib.Test/DiagonalWallCollisionShapeTests.cs
Original file line number Diff line number Diff line change
@@ -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<int>(roomSectorShapeType, shape, flatHeightField);

Assert.AreEqual(flatHeight, GetField<int>(roomSectorShapeType, shape, firstFlattenedField));
Assert.AreEqual(flatHeight, GetField<int>(roomSectorShapeType, shape, secondFlattenedField));
Assert.AreEqual(0, GetField<int>(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<int>(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<T>(Type roomSectorShapeType, object instance, string fieldName)
=> (T)roomSectorShapeType
.GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!
.GetValue(instance)!;
}
77 changes: 58 additions & 19 deletions TombLib/TombLib/LevelData/Compilers/FloorData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -864,42 +891,54 @@ 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;
break;
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));
Expand Down
77 changes: 58 additions & 19 deletions TombLib/TombLib/LevelData/Compilers/TombEngine/FloorData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -443,42 +470,54 @@ 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;
break;
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));
Expand Down