Ajout du projet Depths sur Git

This commit is contained in:
2026-04-30 12:24:52 +02:00
commit a143ea22c7
6651 changed files with 77423 additions and 0 deletions
@@ -0,0 +1,264 @@
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "CoreTypes.h"
#include "Containers/UnrealString.h"
#include "Misc/AutomationTest.h"
#include "ProceduralDungeonTypes.h"
#include "TestUtils.h"
#if WITH_DEV_AUTOMATION_TESTS
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FBoxMinAndMaxTest, "ProceduralDungeon.Types.BoxMinAndMax", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::SmokeFilter)
bool FBoxMinAndMaxTest::RunTest(const FString& Parameters)
{
// Constructor Test
{
FBoxMinAndMax Box0;
FBoxMinAndMax Box1(FIntVector(-1), FIntVector(1));
FBoxMinAndMax Box2(FIntVector(3, 2, 1), FIntVector(-1, -2, -3));
FBoxMinAndMax Box3(Box2);
FBoxMinAndMax Box4(FIntVector(-3, 2, -1), FIntVector(1, -2, 3));
FBoxMinAndMax Box5 = FBoxMinAndMax::Invalid;
TestEqual(TEXT("Default Constructor Min == 0"), Box0.GetMin(), FIntVector(0));
TestEqual(TEXT("Default Constructor Max == 1"), Box0.GetMax(), FIntVector(1));
TestEqual(TEXT("Constructor (-1,1) Min == -1"), Box1.GetMin(), FIntVector(-1));
TestEqual(TEXT("Constructor (-1,1) Max == 1"), Box1.GetMax(), FIntVector(1));
TestEqual(TEXT("Constructor ((3,2,1), (-1,-2,-3)) Min == (-1,-2,-3)"), Box2.GetMin(), FIntVector(-1, -2, -3));
TestEqual(TEXT("Constructor ((3,2,1), (-1,-2,-3)) Max == (3,2,1)"), Box2.GetMax(), FIntVector(3, 2, 1));
TestEqual(TEXT("Copy Constructor of ((3,2,1), (-1,-2,-3)) Min == (-1,-2,-3)"), Box3.GetMin(), FIntVector(-1, -2, -3));
TestEqual(TEXT("Copy Constructor of ((3,2,1), (-1,-2,-3)) Max == (3,2,1)"), Box3.GetMax(), FIntVector(3, 2, 1));
TestEqual(TEXT("Constructor ((-3,2,-1), (1,-2,3)) Min == (-3,-2,-1)"), Box4.GetMin(), FIntVector(-3, -2, -1));
TestEqual(TEXT("Constructor ((-3,2,-1), (1,-2,3)) Max == (1,2,3)"), Box4.GetMax(), FIntVector(1, 2, 3));
TestEqual(TEXT("Invalid Box Max == 0"), Box5.GetMin(), FIntVector(0));
TestEqual(TEXT("Invalid Box Max == 0"), Box5.GetMax(), FIntVector(0));
TestFalse(TEXT("Invalid Box is not Valid"), Box5.IsValid());
}
// Size Test
{
FBoxMinAndMax Box0(FIntVector(0), FIntVector(0));
FBoxMinAndMax Box1(FIntVector(0), FIntVector(1));
FBoxMinAndMax Box2(FIntVector(0), FIntVector(-1));
FBoxMinAndMax Box3(FIntVector(-1), FIntVector(0));
FBoxMinAndMax Box4(FIntVector(1), FIntVector(0));
FBoxMinAndMax Box5(FIntVector(0, 0, 0), FIntVector(3, 4, 5));
FBoxMinAndMax Box6(FIntVector(3, 4, 5), FIntVector(-5, -4, -3));
TestEqual(TEXT("Box(0, 0).GetSize() == (0,0,0)"), Box0.GetSize(), FIntVector(0));
TestEqual(TEXT("Box(0, 1).GetSize() == (1,1,1)"), Box1.GetSize(), FIntVector(1));
TestEqual(TEXT("Box(0, -1).GetSize() == (1,1,1)"), Box2.GetSize(), FIntVector(1));
TestEqual(TEXT("Box(-1, 0).GetSize() == (1,1,1)"), Box3.GetSize(), FIntVector(1));
TestEqual(TEXT("Box(1, 0).GetSize() == (1,1,1)"), Box4.GetSize(), FIntVector(1));
TestEqual(TEXT("Box(0, (3, 4, 5)).GetSize() == (3,4,5)"), Box5.GetSize(), FIntVector(3, 4, 5));
TestEqual(TEXT("Box((3, 4, 5), (-5, -4, -3)).GetSize() == (8,8,8)"), Box6.GetSize(), FIntVector(8));
}
// Overlap Test
{
FBoxMinAndMax Box1(FIntVector(-2, -3, -4), FIntVector(2, 3, 4));
FBoxMinAndMax Box2(FIntVector(3, 0, 0), FIntVector(5, 5, 5));
FBoxMinAndMax Box3(FIntVector(2, 3, 0), FIntVector(5, 5, 5));
FBoxMinAndMax Box4(FIntVector(2, 2, 0), FIntVector(5, 5, 5));
FBoxMinAndMax Box5(FIntVector(1, 2, 0), FIntVector(5, 5, 5));
FBoxMinAndMax Box6(FIntVector(-3, 0, 0), FIntVector(-5, -5, -5));
FBoxMinAndMax Box7(FIntVector(-2, -2, 0), FIntVector(-5, -5, -5));
FBoxMinAndMax Box8(FIntVector(-1, -2, 0), FIntVector(-5, -5, -5));
TestTrue(TEXT("Box((-2,-3,-4), (2,3,4)) and Box((-2,-3,-4), (2,3,4)) overlap"), FBoxMinAndMax::Overlap(Box1, Box1));
TestFalse(TEXT("Box((-2,-3,-4), (2,3,4)) and Box((3,0,0), (5,5,5)) don't overlap"), FBoxMinAndMax::Overlap(Box1, Box2));
TestFalse(TEXT("Box((-2,-3,-4), (2,3,4)) and Box((2,3,0), (5,5,5)) don't overlap"), FBoxMinAndMax::Overlap(Box1, Box3));
TestFalse(TEXT("Box((-2,-3,-4), (2,3,4)) and Box((2,2,0), (5,5,5)) don't overlap"), FBoxMinAndMax::Overlap(Box1, Box4));
TestTrue(TEXT("Box((-2,-3,-4), (2,3,4)) and Box((1,2,0), (5,5,5)) overlap"), FBoxMinAndMax::Overlap(Box1, Box5));
TestFalse(TEXT("Box((-2,-3,-4), (2,3,4)) and Box((-3,0,0), (-5,-5,-5)) don't overlap"), FBoxMinAndMax::Overlap(Box1, Box6));
TestFalse(TEXT("Box((-2,-3,-4), (2,3,4)) and Box((-2,-2,0), (-5,-5,-5)) don't overlap"), FBoxMinAndMax::Overlap(Box1, Box7));
TestTrue(TEXT("Box((-2,-3,-4), (2,3,4)) and Box((-1,-2,0), (-5,-5,-5)) overlap"), FBoxMinAndMax::Overlap(Box1, Box8));
FBoxMinAndMax BoxA(FIntVector(0), FIntVector(1));
FBoxMinAndMax BoxB(FIntVector(-2), FIntVector(5));
TestTrue(TEXT("Box(0, 1) and Box(-2, 5) overlap"), FBoxMinAndMax::Overlap(BoxA, BoxB));
}
// Rotation Test
{
FBoxMinAndMax Box0(FIntVector(0), FIntVector(1));
FBoxMinAndMax RotBox0N = Rotate(Box0, EDoorDirection::North);
FBoxMinAndMax RotBox0E = Rotate(Box0, EDoorDirection::East);
FBoxMinAndMax RotBox0S = Rotate(Box0, EDoorDirection::South);
FBoxMinAndMax RotBox0W = Rotate(Box0, EDoorDirection::West);
TestEqual(TEXT("Rotate(Box(0,1), N).Min == 0"), RotBox0N.GetMin(), FIntVector(0));
TestEqual(TEXT("Rotate(Box(0,1), N).Max == 1"), RotBox0N.GetMax(), FIntVector(1));
TestEqual(TEXT("Rotate(Box(0,1), E).Min == 0"), RotBox0E.GetMin(), FIntVector(0));
TestEqual(TEXT("Rotate(Box(0,1), E).Max == 1"), RotBox0E.GetMax(), FIntVector(1));
TestEqual(TEXT("Rotate(Box(0,1), S).Min == 0"), RotBox0S.GetMin(), FIntVector(0));
TestEqual(TEXT("Rotate(Box(0,1), S).Max == 1"), RotBox0S.GetMax(), FIntVector(1));
TestEqual(TEXT("Rotate(Box(0,1), W).Min == 0"), RotBox0W.GetMin(), FIntVector(0));
TestEqual(TEXT("Rotate(Box(0,1), W).Max == 1"), RotBox0W.GetMax(), FIntVector(1));
FBoxMinAndMax Box1(FIntVector(-1, 0, -1), FIntVector(3, 1, 1));
FBoxMinAndMax RotBox1N = Rotate(Box1, EDoorDirection::North);
FBoxMinAndMax RotBox1E = Rotate(Box1, EDoorDirection::East);
FBoxMinAndMax RotBox1S = Rotate(Box1, EDoorDirection::South);
FBoxMinAndMax RotBox1W = Rotate(Box1, EDoorDirection::West);
TestEqual(TEXT("Rotate(Box((-1,0,-1), (3,1,1)), N).Min == (-1,0,-1)"), RotBox1N.GetMin(), FIntVector(-1, 0, -1));
TestEqual(TEXT("Rotate(Box((-1,0,-1), (3,1,1)), N).Max == (3,1,1)"), RotBox1N.GetMax(), FIntVector(3, 1, 1));
TestEqual(TEXT("Rotate(Box((-1,0,-1), (3,1,1)), E).Min == (0,-1,-1)"), RotBox1E.GetMin(), FIntVector(0, -1, -1));
TestEqual(TEXT("Rotate(Box((-1,0,-1), (3,1,1)), E).Max == (1,3,1)"), RotBox1E.GetMax(), FIntVector(1, 3, 1));
TestEqual(TEXT("Rotate(Box((-1,0,-1), (3,1,1)), S).Min == (-2,0,-1)"), RotBox1S.GetMin(), FIntVector(-2, 0, -1));
TestEqual(TEXT("Rotate(Box((-1,0,-1), (3,1,1)), S).Max == (2,1,1)"), RotBox1S.GetMax(), FIntVector(2, 1, 1));
TestEqual(TEXT("Rotate(Box((-1,0,-1), (3,1,1)), W).Min == (0,-2,-1)"), RotBox1W.GetMin(), FIntVector(0, -2, -1));
TestEqual(TEXT("Rotate(Box((-1,0,-1), (3,1,1)), W).Max == (1,2,1)"), RotBox1W.GetMax(), FIntVector(1, 2, 1));
}
// Extend Test
{
FBoxMinAndMax BoxToExtend;
FBoxMinAndMax Box0({0, 0, 0}, {1, 1, 1});
FBoxMinAndMax Box1 = Box0 + FIntVector(1, 2, 3); // offseted box
FBoxMinAndMax Box2({-1, -2, -3}, {4, 5, 6});
BoxToExtend.Extend(Box0);
TestEqual(TEXT("Extend Box Step 1"), BoxToExtend, Box0);
// Extend the box to contain the provided box
BoxToExtend.Extend(Box1);
TestEqual(TEXT("Extend Box Step 2"), BoxToExtend, FBoxMinAndMax({0, 0, 0}, {2, 3, 4}));
BoxToExtend.Extend(Box2);
TestEqual(TEXT("Extend Box Step 3"), BoxToExtend, Box2);
// The extended box should not change when using a box entirely contained in it.
BoxToExtend.Extend(Box0);
TestEqual(TEXT("Extend Box Step 4"), BoxToExtend, Box2);
}
// IsInside(FBoxMinAndMax) Test
{
FBoxMinAndMax Bounds(FIntVector(-4, -5, -6), FIntVector(7, 8, 9));
FBoxMinAndMax Box(FIntVector(-1, -1, -1), FIntVector(1, 2, 3));
// Completely inside, no coincident face
TestTrue(*FString::Printf(TEXT("Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
// Positive X
Box += FIntVector(6, 0, 0); // Inside but with coincident face
TestTrue(*FString::Printf(TEXT("[X+,A] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
Box += FIntVector(1, 0, 0); // Intersecting
TestFalse(*FString::Printf(TEXT("[X+,B] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
Box += FIntVector(1, 0, 0); // Outside but with a coincident face
TestFalse(*FString::Printf(TEXT("[X+,C] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
// Reset
Box = FBoxMinAndMax(FIntVector(-1, -1, -1), FIntVector(1, 2, 3));
// Positive Y
Box += FIntVector(0, 6, 0); // Inside but with coincident face
TestTrue(*FString::Printf(TEXT("[Y+,A] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
Box += FIntVector(0, 1, 0); // Intersecting
TestFalse(*FString::Printf(TEXT("[Y+,B] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
Box += FIntVector(0, 1, 0); // Outside but with a coincident face
TestFalse(*FString::Printf(TEXT("[Y+,C] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
// Reset
Box = FBoxMinAndMax(FIntVector(-1, -1, -1), FIntVector(1, 2, 3));
// Positive Z
Box += FIntVector(0, 0, 6); // Inside but with coincident face
TestTrue(*FString::Printf(TEXT("[Z+,A] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
Box += FIntVector(0, 0, 1); // Intersecting
TestFalse(*FString::Printf(TEXT("[Z+,B] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
Box += FIntVector(0, 0, 1); // Outside but with a coincident face
TestFalse(*FString::Printf(TEXT("[Z+,C] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
// Reset
Box = FBoxMinAndMax(FIntVector(-1, -1, -1), FIntVector(1, 2, 3));
// Negative X
Box += FIntVector(-3, 0, 0); // Inside but with coincident face
TestTrue(*FString::Printf(TEXT("[X-,A] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
Box += FIntVector(-1, 0, 0); // Intersecting
TestFalse(*FString::Printf(TEXT("[X-,B] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
Box += FIntVector(-1, 0, 0); // Outside but with a coincident face
TestFalse(*FString::Printf(TEXT("[X-,C] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
// Reset
Box = FBoxMinAndMax(FIntVector(-1, -1, -1), FIntVector(1, 2, 3));
// Negative Y
Box += FIntVector(0, -4, 0); // Inside but with coincident face
TestTrue(*FString::Printf(TEXT("[Y-,A] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
Box += FIntVector(0, -2, 0); // Intersecting
TestFalse(*FString::Printf(TEXT("[Y-,B] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
Box += FIntVector(0, -1, 0); // Outside but with a coincident face
TestFalse(*FString::Printf(TEXT("[Y-,C] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
// Reset
Box = FBoxMinAndMax(FIntVector(-1, -1, -1), FIntVector(1, 2, 3));
// Negative Z
Box += FIntVector(0, 0, -5); // Inside but with coincident face
TestTrue(*FString::Printf(TEXT("[Z-,A] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
Box += FIntVector(0, 0, -3); // Intersecting
TestFalse(*FString::Printf(TEXT("[Z-,B] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
Box += FIntVector(0, 0, -1); // Outside but with a coincident face
TestFalse(*FString::Printf(TEXT("[Z-,C] Box%s IsInside Bounds%s"), *Box.ToString(), *Bounds.ToString()), Bounds.IsInside(Box));
}
// IsInside(FIntVector) Test
{
FBoxMinAndMax Bounds(FIntVector(-4, -5, -6), FIntVector(7, 8, 9));
FIntVector Cell {0};
// Completely inside, no coincident face
TestTrue(*FString::Printf(TEXT("Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
// Positive X
Cell = {6, 0, 0}; // Inside but with coincident face
TestTrue(*FString::Printf(TEXT("[X+,A] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
Cell = {7, 0, 0}; // Outside but with a coincident face
TestFalse(*FString::Printf(TEXT("[X+,B] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
// Negative X
Cell = {-4, 0, 0}; // Inside but with coincident face
TestTrue(*FString::Printf(TEXT("[X-,A] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
Cell = {-5, 0, 0}; // Outside but with a coincident face
TestFalse(*FString::Printf(TEXT("[X-,B] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
// Positive Y
Cell = {0, 7, 0}; // Inside but with coincident face
TestTrue(*FString::Printf(TEXT("[Y+,A] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
Cell = {0, 8, 0}; // Outside but with a coincident face
TestFalse(*FString::Printf(TEXT("[Y+,B] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
// Negative Y
Cell = {0, -5, 0}; // Inside but with coincident face
TestTrue(*FString::Printf(TEXT("[Y-,A] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
Cell = {0, -6, 0}; // Outside but with a coincident face
TestFalse(*FString::Printf(TEXT("[Y-,B] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
// Positive Z
Cell = {0, 0, 8}; // Inside but with coincident face
TestTrue(*FString::Printf(TEXT("[Z+,A] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
Cell = {0, 0, 9}; // Outside but with a coincident face
TestFalse(*FString::Printf(TEXT("[Z+,B] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
// Negative Z
Cell = {0, 0, -6}; // Inside but with coincident face
TestTrue(*FString::Printf(TEXT("[Z-,A] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
Cell = {0, 0, -7}; // Outside but with a coincident face
TestFalse(*FString::Printf(TEXT("[Z-,B] Cell(%s) IsInside Bounds%s"), *Cell.ToString(), *Bounds.ToString()), Bounds.IsInside(Cell));
}
return true;
}
#endif //WITH_DEV_AUTOMATION_TESTS
@@ -0,0 +1,31 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "VoxelBounds/VoxelBounds.h"
#include "CustomScoreCallbacks.generated.h"
UCLASS(NotBlueprintable, NotBlueprintType, HideDropdown, meta = (HiddenNode))
class UCustomScoreCallback : public UObject
{
GENERATED_BODY()
public:
UFUNCTION()
bool ZeroScore(const FVoxelBoundsConnection& A, const FVoxelBoundsConnection& B, int32& Score)
{
Score = 0;
return true;
}
UFUNCTION()
bool NeverPass(const FVoxelBoundsConnection& A, const FVoxelBoundsConnection& B, int32& Score)
{
return false;
}
};
@@ -0,0 +1,61 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "CoreMinimal.h"
#include "Interfaces/DungeonCustomSerialization.h"
#include "Interfaces/DungeonSaveInterface.h"
#include "Utils/CompatUtils.h"
#include "DungeonSaveClasses.generated.h"
UCLASS(NotBlueprintable, NotBlueprintType, HideDropdown, meta = (HiddenNode))
class USaveTestObject : public UObject, public IDungeonCustomSerialization, public IDungeonSaveInterface
{
GENERATED_BODY()
public:
//~ Begin IDungeonCustomSerialization Interface
virtual bool SerializeObject(FStructuredArchive::FRecord& Record, bool bIsLoading) override
{
OrderOfExecution += (bIsLoading) ? TEXT("X") : TEXT("C");
Record.EnterField(AR_FIELD_NAME("NativeTest")) << TestSerializeObjectFunction;
return true;
}
//~ End IDungeonCustomSerialization Interface
//~ Begin IDungeonSaveInterface Interface
virtual void PreSaveDungeon_Implementation() override
{
OrderOfExecution += TEXT("A");
}
virtual void DungeonPreSerialize_Implementation(bool bIsLoading) override
{
OrderOfExecution += (bIsLoading) ? TEXT("W") : TEXT("B");
}
virtual void DungeonPostSerialize_Implementation(bool bIsLoading) override
{
OrderOfExecution += (bIsLoading) ? TEXT("Y") : TEXT("D");
}
virtual void PostLoadDungeon_Implementation() override
{
OrderOfExecution += TEXT("Z");
}
//~ End IDungeonSaveInterface Interface
public:
UPROPERTY(SaveGame)
int32 TestSaveGameFlag {0};
int32 TestSerializeObjectFunction {0};
UPROPERTY();
FString OrderOfExecution {};
};
@@ -0,0 +1,35 @@
// Copyright Benoit Pelletier 2025 - 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "RoomConstraints/RoomConstraint.h"
#include "RoomConstraintChildClasses.generated.h"
UCLASS(NotBlueprintable, NotBlueprintType, HideDropdown, meta = (HiddenNode))
class UConstraintPass : public URoomConstraint
{
GENERATED_BODY()
public:
virtual bool Check_Implementation(const UDungeonGraph* Dungeon, const URoomData* RoomData, FIntVector Location, EDoorDirection Direction) const override
{
return true;
}
};
UCLASS(NotBlueprintable, NotBlueprintType, HideDropdown, meta = (HiddenNode))
class UConstraintFail : public URoomConstraint
{
GENERATED_BODY()
public:
virtual bool Check_Implementation(const UDungeonGraph* Dungeon, const URoomData* RoomData, FIntVector Location, EDoorDirection Direction) const override
{
return false;
}
};
@@ -0,0 +1,29 @@
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "RoomCustomData.h"
#include "RoomCustomDataChildClasses.generated.h"
UCLASS(NotBlueprintable, NotBlueprintType, HideDropdown, meta = (HiddenNode))
class UCustomDataA : public URoomCustomData
{
GENERATED_BODY()
};
UCLASS(NotBlueprintable, NotBlueprintType, HideDropdown, meta = (HiddenNode))
class UCustomDataB : public URoomCustomData
{
GENERATED_BODY()
};
UCLASS(NotBlueprintable, NotBlueprintType, HideDropdown, meta = (HiddenNode))
class UCustomDataC : public URoomCustomData
{
GENERATED_BODY()
};
@@ -0,0 +1,91 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "CoreTypes.h"
#include "Containers/UnrealString.h"
#include "Misc/AutomationTest.h"
#include "ProceduralDungeonTypes.h"
#include "DoorType.h"
#include "TestUtils.h"
#if WITH_DEV_AUTOMATION_TESTS
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDoorDefTest, "ProceduralDungeon.Types.DoorDef", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::SmokeFilter)
bool FDoorDefTest::RunTest(const FString& Parameters)
{
CREATE_DATA_ASSET(UDoorType, DoorTypeA);
CREATE_DATA_ASSET(UDoorType, DoorTypeB);
// Constructor Test
{
FDoorDef Door0;
FDoorDef Door1(FDoorDef::Invalid);
FDoorDef Door2({1, 2, 3}, EDoorDirection::South, DoorTypeA.Get());
FDoorDef Door3(Door2);
TestTrue(TEXT("Default Constructor makes valid door"), Door0.IsValid());
TestFalse(TEXT("Copy constructor of invalid door mus makes an invalid door"), Door1.IsValid());
TestTrue(TEXT("Constructor (1,2,3) South DoorTypeA is valid"), Door2.IsValid());
TestEqual(TEXT("Constructor (1,2,3) South DoorTypeA :: Position == (1,2,3)"), Door2.Position, {1, 2, 3});
TestEqual(TEXT("Constructor (1,2,3) South DoorTypeA :: Direction == South"), Door2.Direction, EDoorDirection::South);
TestEqual(TEXT("Constructor (1,2,3) South DoorTypeA :: Type == DoorTypeA"), Door2.Type, DoorTypeA.Get());
TestTrue(TEXT("Copy Constructor of valid door must be valid too"), Door3.IsValid());
TestEqual(TEXT("Copy Constructor must carry over position"), Door3.Position, Door2.Position);
TestEqual(TEXT("Copy Constructor must carry over direction"), Door3.Direction, Door2.Direction);
TestEqual(TEXT("Copy Constructor must carry over type"), Door3.Type, Door2.Type);
}
// Compatibility Test
{
FDoorDef Door0({0, 0, 0}, EDoorDirection::North, DoorTypeA.Get());
FDoorDef Door1({1, 2, 3}, EDoorDirection::South, DoorTypeA.Get());
FDoorDef Door2({1, 2, 3}, EDoorDirection::South, DoorTypeB.Get());
FDoorDef Door3;
TestTrue(TEXT("Door0 is compatible with Door1"), FDoorDef::AreCompatible(Door0, Door1));
TestFalse(TEXT("Door0 is not compatible with Door2"), FDoorDef::AreCompatible(Door0, Door2));
TestFalse(TEXT("Door0 is not compatible with Door3"), FDoorDef::AreCompatible(Door0, Door3));
TestFalse(TEXT("Door1 is not compatible with Door2"), FDoorDef::AreCompatible(Door1, Door2));
TestFalse(TEXT("Door1 is not compatible with Door3"), FDoorDef::AreCompatible(Door1, Door3));
TestFalse(TEXT("Door2 is not compatible with Door3"), FDoorDef::AreCompatible(Door2, Door3));
}
// Opposite Test
{
FDoorDef Door0({1, 2, 3}, EDoorDirection::North, DoorTypeA.Get());
FDoorDef Door1 = Door0.GetOpposite();
TestTrue(TEXT("Opposite door is valid"), Door1.IsValid());
TestEqual(TEXT("Opposite of North is South"), Door1.Direction, EDoorDirection::South);
TestEqual(TEXT("Opposite cell of (1,2,3)[North] is (2,2,3)"), Door1.Position, {2, 2, 3});
TestEqual(TEXT("Opposite type is the same"), Door1.Type, DoorTypeA.Get());
TestTrue(TEXT("Opposite door is compatible with original"), FDoorDef::AreCompatible(Door0, Door1));
}
// Transform Test
{
FDoorDef Door0({1, 2, 3}, EDoorDirection::North, DoorTypeA.Get());
FDoorDef TransformedDoor0({-1, 3, 6}, EDoorDirection::East, DoorTypeA.Get());
FDoorDef TransformedDoor1({0, 0, 6}, EDoorDirection::South, DoorTypeA.Get());
FDoorDef TransformedDoor2({3, 1, 6}, EDoorDirection::West, DoorTypeA.Get());
TestEqual(TEXT("Transformation {(1, 2, 3), East} is correct"), FDoorDef::Transform(Door0, {1, 2, 3}, EDoorDirection::East), TransformedDoor0);
TestEqual(TEXT("Transformation {(1, 2, 3), South} is correct"), FDoorDef::Transform(Door0, {1, 2, 3}, EDoorDirection::South), TransformedDoor1);
TestEqual(TEXT("Transformation {(1, 2, 3), West} is correct"), FDoorDef::Transform(Door0, {1, 2, 3}, EDoorDirection::West), TransformedDoor2);
TestEqual(TEXT("Inverse Transformation {(1, 2, 3), East} is correct"), FDoorDef::InverseTransform(TransformedDoor0, {1, 2, 3}, EDoorDirection::East), Door0);
TestEqual(TEXT("Inverse Transformation {(1, 2, 3), South} is correct"), FDoorDef::InverseTransform(TransformedDoor1, {1, 2, 3}, EDoorDirection::South), Door0);
TestEqual(TEXT("Inverse Transformation {(1, 2, 3), West} is correct"), FDoorDef::InverseTransform(TransformedDoor2, {1, 2, 3}, EDoorDirection::West), Door0);
}
return true;
}
#endif //WITH_DEV_AUTOMATION_TESTS
@@ -0,0 +1,109 @@
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "CoreTypes.h"
#include "Containers/UnrealString.h"
#include "Misc/AutomationTest.h"
#include "ProceduralDungeonTypes.h"
#include "TestUtils.h"
#if WITH_DEV_AUTOMATION_TESTS
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDoorDirectionTest, "ProceduralDungeon.Types.DoorDirection", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::SmokeFilter)
bool FDoorDirectionTest::RunTest(const FString& Parameters)
{
// Adding directions is correct
{
TestEqual(TEXT("North + North = North"), EDoorDirection::North + EDoorDirection::North, EDoorDirection::North);
TestEqual(TEXT("North + East = East"), EDoorDirection::North + EDoorDirection::East, EDoorDirection::East);
TestEqual(TEXT("North + South = South"), EDoorDirection::North + EDoorDirection::South, EDoorDirection::South);
TestEqual(TEXT("North + West = West"), EDoorDirection::North + EDoorDirection::West, EDoorDirection::West);
TestEqual(TEXT("East + North = East"), EDoorDirection::East + EDoorDirection::North, EDoorDirection::East);
TestEqual(TEXT("East + East = South"), EDoorDirection::East + EDoorDirection::East, EDoorDirection::South);
TestEqual(TEXT("East + South = West"), EDoorDirection::East + EDoorDirection::South, EDoorDirection::West);
TestEqual(TEXT("East + West = North"), EDoorDirection::East + EDoorDirection::West, EDoorDirection::North);
TestEqual(TEXT("South + North = South"), EDoorDirection::South + EDoorDirection::North, EDoorDirection::South);
TestEqual(TEXT("South + East = West"), EDoorDirection::South + EDoorDirection::East, EDoorDirection::West);
TestEqual(TEXT("South + South = North"), EDoorDirection::South + EDoorDirection::South, EDoorDirection::North);
TestEqual(TEXT("South + West = East"), EDoorDirection::South + EDoorDirection::West, EDoorDirection::East);
TestEqual(TEXT("West + North = West"), EDoorDirection::West + EDoorDirection::North, EDoorDirection::West);
TestEqual(TEXT("West + East = North"), EDoorDirection::West + EDoorDirection::East, EDoorDirection::North);
TestEqual(TEXT("West + South = East"), EDoorDirection::West + EDoorDirection::South, EDoorDirection::East);
TestEqual(TEXT("West + West = South"), EDoorDirection::West + EDoorDirection::West, EDoorDirection::South);
}
// Subtracting directions is correct
{
TestEqual(TEXT("North - North = North"), EDoorDirection::North - EDoorDirection::North, EDoorDirection::North);
TestEqual(TEXT("North - East = West"), EDoorDirection::North - EDoorDirection::East, EDoorDirection::West);
TestEqual(TEXT("North - South = South"), EDoorDirection::North - EDoorDirection::South, EDoorDirection::South);
TestEqual(TEXT("North - West = East"), EDoorDirection::North - EDoorDirection::West, EDoorDirection::East);
TestEqual(TEXT("East - North = East"), EDoorDirection::East - EDoorDirection::North, EDoorDirection::East);
TestEqual(TEXT("East - East = North"), EDoorDirection::East - EDoorDirection::East, EDoorDirection::North);
TestEqual(TEXT("East - South = West"), EDoorDirection::East - EDoorDirection::South, EDoorDirection::West);
TestEqual(TEXT("East - West = South"), EDoorDirection::East - EDoorDirection::West, EDoorDirection::South);
TestEqual(TEXT("South - North = South"), EDoorDirection::South - EDoorDirection::North, EDoorDirection::South);
TestEqual(TEXT("South - East = East"), EDoorDirection::South - EDoorDirection::East, EDoorDirection::East);
TestEqual(TEXT("South - South = North"), EDoorDirection::South - EDoorDirection::South, EDoorDirection::North);
TestEqual(TEXT("South - West = West"), EDoorDirection::South - EDoorDirection::West, EDoorDirection::West);
TestEqual(TEXT("West - North = West"), EDoorDirection::West - EDoorDirection::North, EDoorDirection::West);
TestEqual(TEXT("West - East = South"), EDoorDirection::West - EDoorDirection::East, EDoorDirection::South);
TestEqual(TEXT("West - South = East"), EDoorDirection::West - EDoorDirection::South, EDoorDirection::East);
TestEqual(TEXT("West - West = North"), EDoorDirection::West - EDoorDirection::West, EDoorDirection::North);
}
// Negating directions are correct
{
TestEqual(TEXT("-North = North"), -EDoorDirection::North, EDoorDirection::North);
TestEqual(TEXT("-East = West"), -EDoorDirection::East, EDoorDirection::West);
TestEqual(TEXT("-South = South"), -EDoorDirection::South, EDoorDirection::South);
TestEqual(TEXT("-West = East"), -EDoorDirection::West, EDoorDirection::East);
}
// Opposite directions are correct
{
TestEqual(TEXT("~North = South"), ~EDoorDirection::North, EDoorDirection::South);
TestEqual(TEXT("~East = West"), ~EDoorDirection::East, EDoorDirection::West);
TestEqual(TEXT("~South = North"), ~EDoorDirection::South, EDoorDirection::North);
TestEqual(TEXT("~West = East"), ~EDoorDirection::West, EDoorDirection::East);
}
// Incrementing/decrementing directions are correct
{
EDoorDirection direction {EDoorDirection::North};
TestEqual(TEXT("++North = East"), ++direction, EDoorDirection::East);
TestEqual(TEXT("++East = South"), ++direction, EDoorDirection::South);
TestEqual(TEXT("++South = West"), ++direction, EDoorDirection::West);
TestEqual(TEXT("++West = North"), ++direction, EDoorDirection::North);
direction = EDoorDirection::North;
TestEqual(TEXT("--North = West"), --direction, EDoorDirection::West);
TestEqual(TEXT("--West = South"), --direction, EDoorDirection::South);
TestEqual(TEXT("--South = East"), --direction, EDoorDirection::East);
TestEqual(TEXT("--East = North"), --direction, EDoorDirection::North);
}
// Boolean testing directions
{
TestFalse(TEXT("!North = false"), !EDoorDirection::North);
TestFalse(TEXT("!East = false"), !EDoorDirection::East);
TestFalse(TEXT("!South = false"), !EDoorDirection::South);
TestFalse(TEXT("!West = false"), !EDoorDirection::West);
TestTrue(TEXT("!NbDirection = true"), !EDoorDirection::NbDirection);
}
return true;
}
#endif //WITH_DEV_AUTOMATION_TESTS
@@ -0,0 +1,90 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "CoreTypes.h"
#include "Containers/UnrealString.h"
#include "Misc/AutomationTest.h"
#include "DoorType.h"
#include "UObject/Package.h"
#include "TestUtils.h"
#if WITH_DEV_AUTOMATION_TESTS
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDoorTypeTests, "ProceduralDungeon.Types.DoorType", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::EngineFilter)
bool FDoorTypeTests::RunTest(const FString& Parameters)
{
// Test DoorType Compatibility
{
CREATE_DATA_ASSET(UDoorType, TypeA);
CREATE_DATA_ASSET(UDoorType, TypeB);
CREATE_DATA_ASSET(UDoorType, TypeC);
/* Default Compatibility Table:
* A B C N
* A O X X X
* B X O X X
* C X X O X
* N X X X O
*/
TestTrue("TypeA should be compatible with itself", UDoorType::AreCompatible(TypeA.Get(), TypeA.Get()));
TestFalse("TypeA should not be compatible with TypeB", UDoorType::AreCompatible(TypeA.Get(), TypeB.Get()));
TestFalse("TypeA should not be compatible with TypeC", UDoorType::AreCompatible(TypeA.Get(), TypeC.Get()));
TestFalse("TypeA should not be compatible with Null", UDoorType::AreCompatible(TypeA.Get(), nullptr));
TestFalse("TypeB should not be compatible with TypeA", UDoorType::AreCompatible(TypeB.Get(), TypeA.Get()));
TestTrue("TypeB should be compatible with itself", UDoorType::AreCompatible(TypeB.Get(), TypeB.Get()));
TestFalse("TypeB should not be compatible with TypeC", UDoorType::AreCompatible(TypeB.Get(), TypeC.Get()));
TestFalse("TypeB should not be compatible with Null", UDoorType::AreCompatible(TypeB.Get(), nullptr));
TestFalse("TypeC should not be compatible with TypeA", UDoorType::AreCompatible(TypeC.Get(), TypeA.Get()));
TestFalse("TypeC should not be compatible with TypeB", UDoorType::AreCompatible(TypeC.Get(), TypeB.Get()));
TestTrue("TypeC should not be compatible with TypeC", UDoorType::AreCompatible(TypeC.Get(), TypeC.Get()));
TestFalse("TypeC should not be compatible with Null", UDoorType::AreCompatible(TypeC.Get(), nullptr));
TestFalse("Null should not be compatible with TypeA", UDoorType::AreCompatible(nullptr, TypeA.Get()));
TestFalse("Null should not be compatible with TypeB", UDoorType::AreCompatible(nullptr, TypeB.Get()));
TestFalse("Null should not be compatible with TypeC", UDoorType::AreCompatible(nullptr, TypeC.Get()));
TestTrue("Null should be compatible with Null", UDoorType::AreCompatible(nullptr, nullptr));
/* Test Compatibility Table:
* A B C N
* A O O X X
* B O X X X
* C X X O X
* N X X X O
*/
TypeA->SetCompatibility({});
TypeB->SetCompatibility({ TypeA.Get() });
TypeB->SetCompatibleWithItself(false);
TypeC->SetCompatibility({});
TestTrue("TypeA should be compatible with itself", UDoorType::AreCompatible(TypeA.Get(), TypeA.Get()));
TestTrue("TypeA should be compatible with TypeB", UDoorType::AreCompatible(TypeA.Get(), TypeB.Get()));
TestFalse("TypeA should not be compatible with TypeC", UDoorType::AreCompatible(TypeA.Get(), TypeC.Get()));
TestFalse("TypeA should not be compatible with Null", UDoorType::AreCompatible(TypeA.Get(), nullptr));
TestTrue("TypeB should be compatible with TypeA", UDoorType::AreCompatible(TypeB.Get(), TypeA.Get()));
TestFalse("TypeB should not be compatible with itself", UDoorType::AreCompatible(TypeB.Get(), TypeB.Get()));
TestFalse("TypeB should not be compatible with TypeC", UDoorType::AreCompatible(TypeB.Get(), TypeC.Get()));
TestFalse("TypeB should not be compatible with Null", UDoorType::AreCompatible(TypeB.Get(), nullptr));
TestFalse("TypeC should not be compatible with TypeA", UDoorType::AreCompatible(TypeC.Get(), TypeA.Get()));
TestFalse("TypeC should not be compatible with TypeB", UDoorType::AreCompatible(TypeC.Get(), TypeB.Get()));
TestTrue("TypeC should be compatible with TypeC", UDoorType::AreCompatible(TypeC.Get(), TypeC.Get()));
TestFalse("TypeC should not be compatible with Null", UDoorType::AreCompatible(TypeC.Get(), nullptr));
TestFalse("Null should not be compatible with TypeA", UDoorType::AreCompatible(nullptr, TypeA.Get()));
TestFalse("Null should not be compatible with TypeB", UDoorType::AreCompatible(nullptr, TypeB.Get()));
TestFalse("Null should not be compatible with TypeC", UDoorType::AreCompatible(nullptr, TypeC.Get()));
TestTrue("Null should be compatible with Null", UDoorType::AreCompatible(nullptr, nullptr));
}
return true;
}
#endif //WITH_DEV_AUTOMATION_TESTS
@@ -0,0 +1,384 @@
// Copyright Benoit Pelletier 2023 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "CoreTypes.h"
#include "Containers/UnrealString.h"
#include "Misc/AutomationTest.h"
#include "DungeonGraph.h"
#include "Room.h"
#include "RoomData.h"
#include "TestUtils.h"
#include "./Classes/RoomConstraintChildClasses.h"
#if WITH_DEV_AUTOMATION_TESTS
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDungeonGraphTest, "ProceduralDungeon.Types.DungeonGraph", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::EngineFilter)
#define INIT_TEST(Graph) \
TStrongObjectPtr<UDungeonGraph> Graph(NewObject<UDungeonGraph>(GetTransientPackage(), #Graph));
#define CLEAN_TEST() \
Graph->Clear();
// Utility to create and initialize a room
#define CREATE_ROOM(Name, RoomDataPtr) \
URoom* Name = NewObject<URoom>(); \
Name->Init(RoomDataPtr.Get(), nullptr, Graph->Count()); \
Graph->AddRoom(Name);
// Utility to create room data
#define CREATE_ROOM_DATA(Data) \
CREATE_DATA_ASSET(URoomData, Data); \
Data->Doors.Empty();
// Utility to create a non-empty path
#define DUMMY_PATH(Path) \
Path.Empty(); \
Path.Add(nullptr);
#pragma optimize("", off)
bool FDungeonGraphTest::RunTest(const FString& Parameters)
{
{
// Creating data assets
CREATE_ROOM_DATA(DA_A);
CREATE_ROOM_DATA(DA_B);
CREATE_ROOM_DATA(DA_C);
CREATE_ROOM_DATA(DA_D);
DA_A->Doors.Add({{0, 0, 0}, EDoorDirection::South});
DA_B->Doors.Add({{0, 0, 0}, EDoorDirection::East});
DA_B->Doors.Add({{0, 0, 0}, EDoorDirection::West});
DA_C->Doors.Add({{0, 0, 0}, EDoorDirection::North});
DA_C->Doors.Add({{0, 0, 0}, EDoorDirection::South});
DA_C->Doors.Add({{0, 0, 0}, EDoorDirection::East});
DA_C->Doors.Add({{0, 0, 0}, EDoorDirection::West});
DA_D->BoundingBoxes[0].SetMinAndMax({0, 0, 0}, {1, 1, 2});
DA_D->Doors.Add({{0, 0, 0}, EDoorDirection::North});
DA_D->Doors.Add({{0, 0, 1}, EDoorDirection::North});
// Test pathfind
{
INIT_TEST(Graph);
// (Rooms are numbered from left to right and top to bottom starting at 0)
// A--C-C-!C-C-B
// | | |
// A-!B B--B-C
// first line
CREATE_ROOM(Room0, DA_A);
CREATE_ROOM(Room1, DA_C);
CREATE_ROOM(Room2, DA_C);
CREATE_ROOM(Room3, DA_C);
CREATE_ROOM(Room4, DA_C);
CREATE_ROOM(Room5, DA_B);
// second line
CREATE_ROOM(Room6, DA_A);
CREATE_ROOM(Room7, DA_B);
CREATE_ROOM(Room8, DA_B);
CREATE_ROOM(Room9, DA_B);
CREATE_ROOM(Room10, DA_C);
Room3->Lock(true);
Room7->Lock(true);
// first line
Graph->Connect(Room0, 0, Room1, 1);
Graph->Connect(Room1, 0, Room2, 1);
Graph->Connect(Room2, 0, Room3, 1);
Graph->Connect(Room3, 0, Room4, 1);
Graph->Connect(Room4, 0, Room5, 1);
// second line
Graph->Connect(Room6, 0, Room7, 0);
Graph->Connect(Room8, 0, Room9, 1);
Graph->Connect(Room9, 0, Room10, 0);
// transversal
Graph->Connect(Room1, 2, Room7, 1);
Graph->Connect(Room2, 2, Room8, 1);
Graph->Connect(Room4, 2, Room10, 1);
// Used to test path output for some scenarios
TArray<const URoom*> Path;
// Check graph state
TestEqual(TEXT("Graph should have 11 rooms"), 11, Graph->Count());
TestEqual(TEXT("Graph should have 2 rooms from data DA_A"), 2, Graph->CountRoomData(DA_A.Get()));
TestEqual(TEXT("Graph should have 4 rooms from data DA_B"), 4, Graph->CountRoomData(DA_B.Get()));
TestEqual(TEXT("Graph should have 5 rooms from data DA_C"), 5, Graph->CountRoomData(DA_C.Get()));
// Check path find with null rooms
DUMMY_PATH(Path);
TestFalse(TEXT("Path should not be found when both rooms are null"), UDungeonGraph::FindPath(nullptr, nullptr, &Path));
TestEqual(TEXT("Path should be empty"), Path.Num(), 0);
DUMMY_PATH(Path);
TestFalse(TEXT("Path should not be found when first room is null"), UDungeonGraph::FindPath(nullptr, Room1, &Path));
TestEqual(TEXT("Path should be empty"), Path.Num(), 0);
DUMMY_PATH(Path);
TestFalse(TEXT("Path should not be found when second room is null"), UDungeonGraph::FindPath(Room0, nullptr, &Path));
TestEqual(TEXT("Path should be empty"), Path.Num(), 0);
// Check path found with same room
TestTrue(TEXT("Path should be found when both rooms are same"), UDungeonGraph::FindPath(Room0, Room0, &Path));
TestEqual(TEXT("Path should have 1 room"), Path.Num(), 1);
TestTrue(TEXT("Path should have Room0"), Path[0] == Room0);
TestTrue(TEXT("Path should be found when both rooms are same (even if locked)"), UDungeonGraph::FindPath(Room3, Room3, &Path));
TestEqual(TEXT("Path should have 1 room"), Path.Num(), 1);
TestTrue(TEXT("Path should have Room3"), Path[0] == Room3);
// Check no path when one or both rooms are locked
DUMMY_PATH(Path);
TestFalse(TEXT("No path should be found between Room0 and Room3"), UDungeonGraph::FindPath(Room0, Room3, &Path));
TestEqual(TEXT("Path should be empty"), Path.Num(), 0);
DUMMY_PATH(Path);
TestFalse(TEXT("No path should be found between Room3 and Room0"), UDungeonGraph::FindPath(Room3, Room0, &Path));
TestEqual(TEXT("Path should be empty"), Path.Num(), 0);
DUMMY_PATH(Path);
TestFalse(TEXT("No path should be found between Room3 and Room7"), UDungeonGraph::FindPath(Room3, Room7, &Path));
TestEqual(TEXT("Path should be empty"), Path.Num(), 0);
// output path has rooms in the correct order
TestTrue(TEXT("Path should be found between Room0 and Room5"), UDungeonGraph::FindPath(Room0, Room5, &Path));
TestEqual(TEXT("Path should have 8 rooms"), Path.Num(), 8);
TestTrue(TEXT("Path should not go through Room3"), !Path.Contains(Room3));
TestEqual(TEXT("Path's 1st room should be Room0"), Path[0], (const URoom*)Room0);
TestTrue(TEXT("Path's 2nd room should be Room1"), Path[1] == Room1);
TestTrue(TEXT("Path's 3rd room should be Room2"), Path[2] == Room2);
TestTrue(TEXT("Path's 4th room should be Room8"), Path[3] == Room8);
TestTrue(TEXT("Path's 5th room should be Room9"), Path[4] == Room9);
TestTrue(TEXT("Path's 6th room should be Room10"), Path[5] == Room10);
TestTrue(TEXT("Path's 7th room should be Room4"), Path[6] == Room4);
TestTrue(TEXT("Path's 8th room should be Room5"), Path[7] == Room5);
// Check pathfind through locked door (not first nor last)
DUMMY_PATH(Path);
TestFalse(TEXT("No path should be found between Room0 and Room6"), UDungeonGraph::FindPath(Room0, Room6, &Path));
TestEqual(TEXT("Path should be empty"), Path.Num(), 0);
TestTrue(TEXT("Path should be found between Room0 and Room6 (when locked rooms allowed)"), UDungeonGraph::FindPath(Room0, Room6, nullptr, /*IgnoreLocked = */ true));
CLEAN_TEST();
}
// Test Voxel Bounds Conversions
{
INIT_TEST(Graph);
// first floor second floor
// C - B - A C - C
// | |
// D D
CREATE_ROOM(Room0, DA_C);
CREATE_ROOM(Room1, DA_B);
CREATE_ROOM(Room2, DA_A);
CREATE_ROOM(Room3, DA_D);
CREATE_ROOM(Room4, DA_C);
CREATE_ROOM(Room5, DA_C);
Room1->Position = {0, 1, 0};
Room2->Position = {0, 2, 0};
Room2->Direction = EDoorDirection::East;
Room3->Position = {-1, 0, 0};
Room4->Position = {0, 0, 1};
Room5->Position = {0, 1, 1};
// Room positions are modified manually, so we need to explicitely rebuild bounds
Graph->RebuildBounds();
// Check Room0 voxel bounds conversion
{
FVoxelBounds ExpectedBounds;
ExpectedBounds.AddCell({0, 0, 0});
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
FVoxelBounds ConvertedBounds = Room0->GetVoxelBounds();
TestEqual(TEXT("Room0 bounds should be as expected"), ConvertedBounds, ExpectedBounds);
}
// Check Room1 voxel bounds conversion
{
FVoxelBounds ExceptedBounds;
ExceptedBounds.AddCell({0, 1, 0});
ExceptedBounds.SetCellConnection({0, 1, 0}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExceptedBounds.SetCellConnection({0, 1, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExceptedBounds.SetCellConnection({0, 1, 0}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExceptedBounds.SetCellConnection({0, 1, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExceptedBounds.SetCellConnection({0, 1, 0}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExceptedBounds.SetCellConnection({0, 1, 0}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
FVoxelBounds ConvertedBounds = Room1->GetVoxelBounds();
TestEqual(TEXT("Room1 bounds should be as expected"), ConvertedBounds, ExceptedBounds);
}
// Check Room2 voxel bounds conversion
{
FVoxelBounds ExpectedBounds;
ExpectedBounds.AddCell({0, 2, 0});
ExpectedBounds.SetCellConnection({0, 2, 0}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 2, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 2, 0}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 2, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 2, 0}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 2, 0}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
FVoxelBounds ConvertedBounds = Room2->GetVoxelBounds();
TestEqual(TEXT("Room2 bounds should be as expected"), ConvertedBounds, ExpectedBounds);
}
// Check Room3 voxel bounds conversion
{
FVoxelBounds ExpectedBounds;
ExpectedBounds.AddCell({-1, 0, 0});
ExpectedBounds.AddCell({-1, 0, 1});
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({-1, 0, 1}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({-1, 0, 1}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({-1, 0, 1}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({-1, 0, 1}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({-1, 0, 1}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
FVoxelBounds ConvertedBounds = Room3->GetVoxelBounds();
TestEqual(TEXT("Room3 bounds should be as expected"), ConvertedBounds, ExpectedBounds);
}
// Check Room4 voxel bounds conversion
{
FVoxelBounds ExpectedBounds;
ExpectedBounds.AddCell({0, 0, 1});
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
FVoxelBounds ConvertedBounds = Room4->GetVoxelBounds();
TestEqual(TEXT("Room4 bounds should be as expected"), ConvertedBounds, ExpectedBounds);
}
// Check Room5 voxel bounds conversion
{
FVoxelBounds ExpectedBounds;
ExpectedBounds.AddCell({0, 1, 1});
ExpectedBounds.SetCellConnection({0, 1, 1}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 1, 1}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 1, 1}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 1, 1}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 1, 1}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 1, 1}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
FVoxelBounds ConvertedBounds = Room5->GetVoxelBounds();
TestEqual(TEXT("Room5 bounds should be as expected"), ConvertedBounds, ExpectedBounds);
}
}
// FilterAndSort Test
{
INIT_TEST(Graph);
// first floor second floor
// C - B - A C - C
// | |
// D D
CREATE_DATA_ASSET(UConstraintPass, Pass);
CREATE_DATA_ASSET(UConstraintFail, Fail);
CREATE_ROOM_DATA(DA_E);
DA_E->BoundingBoxes[0].SetMinAndMax({0, 0, 0}, {1, 2, 1});
DA_E->Doors.Add({{0, 1, 0}, EDoorDirection::North});
DA_E->Constraints.Add(Pass.Get());
// Same as DA_E but constraint fail
CREATE_ROOM_DATA(DA_F);
DA_F->BoundingBoxes[0].SetMinAndMax({0, 0, 0}, {1, 2, 1});
DA_F->Doors.Add({{0, 1, 0}, EDoorDirection::North});
DA_F->Constraints.Add(Fail.Get());
CREATE_ROOM(Room0, DA_C);
CREATE_ROOM(Room1, DA_B);
CREATE_ROOM(Room2, DA_A);
CREATE_ROOM(Room3, DA_D);
CREATE_ROOM(Room4, DA_C);
CREATE_ROOM(Room5, DA_C);
Room1->Position = {0, 1, 0};
Room2->Position = {0, 2, 0};
Room2->Direction = EDoorDirection::East;
Room3->Position = {-1, 0, 0};
Room4->Position = {0, 0, 1};
Room5->Position = {0, 1, 1};
// Room positions are modified manually, so we need to explicitely rebuild bounds
Graph->RebuildBounds();
const TArray<URoomData*> RoomList = {DA_A.Get(), DA_D.Get(), DA_E.Get(), DA_F.Get()};
TArray<FRoomCandidate> SortedRooms;
{
FDoorDef FromDoor = {{0, 0, 0}, EDoorDirection::North};
bool bHasCandidates = Graph->FilterAndSortRooms(RoomList, FromDoor, SortedRooms);
TestTrue(TEXT("There should be candidates"), bHasCandidates);
TestEqual(TEXT("There should be 4 candidates"), SortedRooms.Num(), 4);
TestEqual(TEXT("RoomData D should be the best candidate"), SortedRooms[0].Data, DA_D.Get());
TestEqual(TEXT("RoomData D Door index 0 should be the best candidate"), SortedRooms[0].DoorIndex, 0);
TestEqual(TEXT("RoomData A should be the worst candidate"), SortedRooms[3].Data, DA_A.Get());
bool bContainsE = false;
bool bContainsF = false;
for (const FRoomCandidate& Candidate : SortedRooms)
{
bContainsE |= (Candidate.Data == DA_E.Get());
bContainsF |= (Candidate.Data == DA_F.Get());
}
// DA_E and DA_F are the same, but DA_F has a failing constraint
TestTrue(TEXT("RoomData E should be a valid candidate"), bContainsE);
TestFalse(TEXT("RoomData F should not be a valid candidate"), bContainsF);
}
{
FDoorDef FromDoor = {{0, 1, 1}, EDoorDirection::South};
bool bHasCandidates = Graph->FilterAndSortRooms(RoomList, FromDoor, SortedRooms);
TestTrue(TEXT("There should be candidates"), bHasCandidates);
TestEqual(TEXT("There should be 3 candidates"), SortedRooms.Num(), 3);
TestEqual(TEXT("RoomData D should be the best candidate"), SortedRooms[0].Data, DA_D.Get());
TestEqual(TEXT("RoomData D DoorIndex 0 should be the best candidate"), SortedRooms[0].DoorIndex, 0);
TestEqual(TEXT("RoomData D should be the worst candidate"), SortedRooms[2].Data, DA_D.Get());
TestEqual(TEXT("RoomData D DoorIndex 1 should be the worst candidate"), SortedRooms[2].DoorIndex, 1);
}
CLEAN_TEST();
}
}
return true;
}
#pragma optimize("", on)
#undef INIT_TEST
#undef CLEAN_TEST
#undef CREATE_ROOM
#undef CREATE_ROOM_DATA
#undef DUMMY_PATH
#endif //WITH_DEV_AUTOMATION_TESTS
@@ -0,0 +1,50 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "CoreTypes.h"
#include "Containers/UnrealString.h"
#include "Misc/AutomationTest.h"
#include "ProceduralDungeonUtils.h"
#include "Interfaces/DungeonSaveInterface.h"
#include "Interfaces/DungeonCustomSerialization.h"
#include "Utils/DungeonSaveUtils.h"
#include "Classes/DungeonSaveClasses.h"
#include "TestUtils.h"
#include "UObject/StrongObjectPtr.h"
#if WITH_DEV_AUTOMATION_TESTS
DEFINE_SPEC(FDungeonSaveSpecs, "ProceduralDungeon.SaveLoad", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::EngineFilter)
void FDungeonSaveSpecs::Define()
{
Describe(TEXT("Interfaces"), [this]()
{
It(TEXT("DungeonSaveInterface"), [this]()
{
TStrongObjectPtr<USaveTestObject> TestSave(NewObject<USaveTestObject>(GetTransientPackage()));
TStrongObjectPtr<USaveTestObject> TestLoad(NewObject<USaveTestObject>(GetTransientPackage()));
TestSave->TestSaveGameFlag = 5;
TestSave->TestSerializeObjectFunction = 8;
TArray<uint8> SavedData {};
SerializeUObject(SavedData, TestSave.Get(), false);
TestEqual(TEXT("should have correct order of execution"), TestSave->OrderOfExecution, FString(TEXT("BCD")));
TestTrue(TEXT("should have written data"), SavedData.Num() > 0);
SerializeUObject(SavedData, TestLoad.Get(), true);
TestEqual(TEXT("should have correct order of execution"), TestLoad->OrderOfExecution, FString(TEXT("WXY")));
TestEqual(TEXT("should have retrieved data from SaveGame flag"), TestLoad->TestSaveGameFlag, TestSave->TestSaveGameFlag);
TestEqual(TEXT("should have retrieved data from SerializedObject function"), TestLoad->TestSerializeObjectFunction, TestSave->TestSerializeObjectFunction);
});
});
}
#endif //WITH_DEV_AUTOMATION_TESTS
@@ -0,0 +1,167 @@
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "CoreTypes.h"
#include "Containers/UnrealString.h"
#include "Misc/AutomationTest.h"
#include "ProceduralDungeonUtils.h"
#include "TestUtils.h"
#if WITH_DEV_AUTOMATION_TESTS
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDungeonUtilsTest_WeightedMap, "ProceduralDungeon.Utils.WeightedMaps", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::SmokeFilter)
bool FDungeonUtilsTest_WeightedMap::RunTest(const FString& Parameters)
{
// Built-in types test
{
TMap<int, int> WeightedMap = {
{1, 0}, // Weight with 0 should never be returned
{2, 1}, // The first non-zero weight should be return for index 0
{3, 2}, // Weights greater than 1 should be returned for as much indices
{4, 1} // The last one should be return when index == total weights minus one
}; // Out of bounds index should return default value
TestEqual(TEXT("Total Weights"), Dungeon::GetTotalWeight(WeightedMap), 4);
TestEqual(TEXT("Weighted value at -1"), Dungeon::GetWeightedAt(WeightedMap, -1), 0); // negative index should return default value
TestEqual(TEXT("Weighted value at 0"), Dungeon::GetWeightedAt(WeightedMap, 0), 2);
TestEqual(TEXT("Weighted value at 1"), Dungeon::GetWeightedAt(WeightedMap, 1), 3);
TestEqual(TEXT("Weighted value at 2"), Dungeon::GetWeightedAt(WeightedMap, 2), 3);
TestEqual(TEXT("Weighted value at 3"), Dungeon::GetWeightedAt(WeightedMap, 3), 4);
TestEqual(TEXT("Weighted value at 4"), Dungeon::GetWeightedAt(WeightedMap, 4), 0); // default int is 0
}
// Pointer test
{
int a = 1;
int b = 2;
int c = 3;
int d = 4;
TMap<int*, int> WeightedMap = {
{&a, 2},
{&b, 0}, // Weight with 0 in middle should be skipped
{&c, 1},
{&d, 0} // The last one should not be returned if weight is 0
};
TestEqual(TEXT("Total Weights"), Dungeon::GetTotalWeight(WeightedMap), 3);
TestEqual(TEXT("Weighted pointer at -1"), Dungeon::GetWeightedAt(WeightedMap, -1), (int*)nullptr);
TestEqual(TEXT("Weighted pointer at 0"), Dungeon::GetWeightedAt(WeightedMap, 0), &a);
TestEqual(TEXT("Weighted pointer at 1"), Dungeon::GetWeightedAt(WeightedMap, 1), &a);
TestEqual(TEXT("Weighted pointer at 2"), Dungeon::GetWeightedAt(WeightedMap, 2), &c);
TestEqual(TEXT("Weighted pointer at 3"), Dungeon::GetWeightedAt(WeightedMap, 3), (int*)nullptr);
}
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDungeonUtilsTest_Guid2Seed, "ProceduralDungeon.Utils.Guid2Seed.Simple", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::SmokeFilter)
bool FDungeonUtilsTest_Guid2Seed::RunTest(const FString& Parameters)
{
// Guid to Random Seed Tests
{
FGuid Guid1(0x12345678U, 0x9ABCDEF0U, 0x0FEDCBA9U, 0x87654321U);
FGuid Guid2(0x08F7E6D5U, 0xC4B3A291U, 0x192A3B4CU, 0x5D6E7F80U);
int64 Salt1 = 1;
int64 Salt2 = 2;
TestTrue(TEXT("Same Guid and Salt should return same Seed (1)"), Random::Guid2Seed(Guid1, Salt1) == Random::Guid2Seed(Guid1, Salt1));
TestTrue(TEXT("Same Guid and Salt should return same Seed (2)"), Random::Guid2Seed(Guid2, Salt2) == Random::Guid2Seed(Guid2, Salt2));
TestTrue(TEXT("Same Guid but different Salt should return different Seeds (1)"), Random::Guid2Seed(Guid1, Salt1) != Random::Guid2Seed(Guid1, Salt2));
TestTrue(TEXT("Same Guid but different Salt should return different Seeds (2)"), Random::Guid2Seed(Guid2, Salt1) != Random::Guid2Seed(Guid2, Salt2));
TestTrue(TEXT("Different Guid but same Salt should return different Seeds (1)"), Random::Guid2Seed(Guid1, Salt1) != Random::Guid2Seed(Guid2, Salt1));
TestTrue(TEXT("Different Guid but same Salt should return different Seeds (2)"), Random::Guid2Seed(Guid1, Salt2) != Random::Guid2Seed(Guid2, Salt2));
TestTrue(TEXT("Different Guid and Salt should return different Seeds (1)"), Random::Guid2Seed(Guid1, Salt1) != Random::Guid2Seed(Guid2, Salt2));
TestTrue(TEXT("Different Guid and Salt should return different Seeds (2)"), Random::Guid2Seed(Guid1, Salt2) != Random::Guid2Seed(Guid2, Salt1));
}
return true;
}
BEGIN_DEFINE_SPEC(FGuid2SeedStatisticalTests, "ProceduralDungeon.Utils.Guid2Seed.Stats", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::EngineFilter)
struct FTestParams
{
FGuid Guid;
int32 NbElements;
int32 NbSamples;
double CriticalValue;
};
using SampleArrayType = int32[];
static TUniquePtr<SampleArrayType> GenerateSamples(const FTestParams& Params)
{
TUniquePtr<SampleArrayType> Occurences(new int32[Params.NbElements] {0});
for (int32 i = 0; i < Params.NbSamples; ++i)
{
// Create a random stream with only the salt modified.
// This will simulate an actor in a room level generating its first random number
// in each of the room instances (where only the room ID changes)
FRandomStream RNG(Random::Guid2Seed(Params.Guid, i));
Occurences[RNG.RandRange(0, Params.NbElements - 1)]++;
}
return Occurences;
}
static bool PassChiSquaredTest(const FTestParams& Params, SampleArrayType Samples)
{
const double Expected = static_cast<double>(Params.NbSamples) / Params.NbElements;
double X2 = 0;
for (int i = 0; i < Params.NbElements; ++i)
{
const double Delta = Samples[i] - Expected;
X2 += (Delta * Delta) / Expected;
}
//UE_LOG(LogTemp, Warning, TEXT("Chi Squared result %d: %f / %f"), Params.NbElements, X2, Params.CriticalValue);
return X2 < Params.CriticalValue;
}
END_DEFINE_SPEC(FGuid2SeedStatisticalTests)
void FGuid2SeedStatisticalTests::Define()
{
Describe(TEXT("Chi Squared Test"), [this]()
{
FTestParams Params;
Params.NbSamples = 1'000'000'000;
TArray<FGuid> GuidsToTest = {
FGuid(),
FGuid(0x12345678U, 0x9ABCDEF0U, 0x0FEDCBA9U, 0x87654321U),
FGuid(0x08F7E6D5U, 0xC4B3A291U, 0x192A3B4CU, 0x5D6E7F80U),
};
for (const auto& Guid : GuidsToTest)
{
Params.Guid = Guid;
Describe(FString::Printf(TEXT("with Guid %s"), *Guid.ToString()), [this, Params]() mutable
{
// The test cases we want to check for each Guid.
// First value is the number of elements in the generated samples.
// Second value is the critical value for the Chi Squared test (with p-value of 5%)
TArray<TTuple<int32, double>> TestCases;
TestCases.Add(TTuple<int32, double>(10, 16.91898));
TestCases.Add(TTuple<int32, double>(100, 123.22522));
TestCases.Add(TTuple<int32, double>(1000, 1073.64265));
for (const auto& TestCase : TestCases)
{
Params.NbElements = TestCase.Key;
Params.CriticalValue = TestCase.Value;
It(FString::Printf(TEXT("with %d elements"), Params.NbElements), [this, Params]()
{
TUniquePtr<SampleArrayType> Samples = GenerateSamples(Params);
TestTrue(TEXT("Pass"), PassChiSquaredTest(Params, Samples.Get()));
});
}
});
}
});
}
#endif //WITH_DEV_AUTOMATION_TESTS
@@ -0,0 +1,448 @@
// Copyright Benoit Pelletier 2024 - 2026 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "CoreTypes.h"
#include "Containers/UnrealString.h"
#include "Misc/AutomationTest.h"
#include "RoomData.h"
#include "DoorType.h"
#include "./Classes/RoomCustomDataChildClasses.h"
#include "./Classes/RoomConstraintChildClasses.h"
#include "UObject/Package.h"
#include "TestUtils.h"
#include "VoxelBounds/VoxelBounds.h"
#if WITH_DEV_AUTOMATION_TESTS
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FRoomDataTests, "ProceduralDungeon.Types.RoomData", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::EngineFilter)
// Utility to create room data
#define CREATE_ROOM_DATA(Data) \
CREATE_DATA_ASSET(URoomData, Data); \
Data->Doors.Empty();
#define ADD_DOOR(ROOM, DOOR_POS, DOOR_DIR, DOOR_TYPE) \
{ \
FDoorDef Door; \
Door.Position = DOOR_POS; \
Door.Direction = DOOR_DIR; \
Door.Type = DOOR_TYPE; \
ROOM->Doors.Add(Door); \
}
bool FRoomDataTests::RunTest(const FString& Parameters)
{
// Test IsRoomInBounds
{
// Creating this room data (X is the room origin, v is the door), thus we could test rotated bounds:
// 1 +---+---+---+
// | | X | |
// 0 +---+---+---+
// | | | |
// -1 +---+---+---+
// | | | |
// -2 +---+-v-+---+
// -1 0 1 2
CREATE_ROOM_DATA(RoomData);
RoomData->BoundingBoxes[0].SetMinAndMax(FIntVector(-2, -1, -1), FIntVector(1, 2, 2));
FDoorDef Door;
Door.Position = FIntVector(-2, 0, 0);
Door.Direction = EDoorDirection::South;
RoomData->Doors.Add(Door);
// If we want to limit the dungeon cells from -2 to 2,
// we need to create a box from -2 to 3 (see below).
// +---+---+---+---+---+
// |-2 |-1 | 0 | 1 | 2 |
// +---+---+---+---+---+
// -2 -1 0 1 2 3
FBoxMinAndMax DungeonBounds({-1000, -2, -1000}, {1000, 3, 1000});
FBoxMinAndMax RoomBoundsAtDoorLocation = RoomData->GetIntBounds() - Door.Position;
// Rotated to South
{
FBoxMinAndMax RotatedRoomBounds = Rotate(RoomBoundsAtDoorLocation, EDoorDirection::North);
TestEqual(TEXT("[S] Rotated Room Bounds: ((0, -1, -1), (3, 2, 2))"), RotatedRoomBounds, FBoxMinAndMax({0, -1, -1}, {3, 2, 2}));
Door.Direction = EDoorDirection::South;
Door.Position = {0, 0, 0};
TestTrue(TEXT("[S] Inside with no coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
// Positive Y
Door.Position = {0, 1, 0};
TestTrue(TEXT("[S] Inside with Y+ as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, 2, 0};
TestFalse(TEXT("[S] Intersecting in Y+"), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, 4, 0};
TestFalse(TEXT("[S] Outside with Y+ as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
// Negative Y
Door.Position = {0, -1, 0};
TestTrue(TEXT("[S] Inside with Y- as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, -2, 0};
TestFalse(TEXT("[S] Intersecting in Y-"), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, -4, 0};
TestFalse(TEXT("[S] Outside with Y- as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
}
// Rotated to East
{
FBoxMinAndMax RotatedRoomBounds = Rotate(RoomBoundsAtDoorLocation, EDoorDirection::West);
TestEqual(TEXT("[E] Rotated Room Bounds: ((-1, -2, -1), (2, 1, 2))"), RotatedRoomBounds, FBoxMinAndMax({-1, -2, -1}, {2, 1, 2}));
Door.Direction = EDoorDirection::East;
Door.Position = {0, 1, 0};
TestTrue(TEXT("[E] Inside with no coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
// Positive Y
Door.Position = {0, 2, 0};
TestTrue(TEXT("[E] Inside with Y+ as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, 3, 0};
TestFalse(TEXT("[E] Intersecting in Y+"), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, 5, 0};
TestFalse(TEXT("[E] Outside with Y+ as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
// Negative Y
Door.Position = {0, 0, 0};
TestTrue(TEXT("[E] Inside with Y- as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, -1, 0};
TestFalse(TEXT("[E] Intersecting in Y-"), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, -3, 0};
TestFalse(TEXT("[E] Outside with Y- as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
}
// Rotated to West
{
FBoxMinAndMax RotatedRoomBounds = Rotate(RoomBoundsAtDoorLocation, EDoorDirection::East);
TestEqual(TEXT("[W] Rotated Room Bounds: ((-1, 0, -1), (2, 3, 2))"), RotatedRoomBounds, FBoxMinAndMax({-1, 0, -1}, {2, 3, 2}));
Door.Direction = EDoorDirection::West;
Door.Position = {0, -1, 0};
TestTrue(TEXT("[W] Inside with no coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
// Positive Y
Door.Position = {0, 0, 0};
TestTrue(TEXT("[W] Inside with Y+ as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, 1, 0};
TestFalse(TEXT("[W] Intersecting in Y+"), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, 3, 0};
TestFalse(TEXT("[W] Outside with Y+ as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
// Negative Y
Door.Position = {0, -2, 0};
TestTrue(TEXT("[W] Inside with Y- as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, -3, 0};
TestFalse(TEXT("[W] Intersecting in Y-"), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, -5, 0};
TestFalse(TEXT("[W] Outside with Y- as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
}
// Rotated to North
{
FBoxMinAndMax RotatedRoomBounds = Rotate(RoomBoundsAtDoorLocation, EDoorDirection::South);
TestEqual(TEXT("[N] Rotated Room Bounds: ((-2, -1, -1), (1, 2, 2))"), RotatedRoomBounds, FBoxMinAndMax({-2, -1, -1}, {1, 2, 2}));
Door.Direction = EDoorDirection::North;
Door.Position = {0, 0, 0};
TestTrue(TEXT("[N] Inside with no coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
// Positive Y
Door.Position = {0, 1, 0};
TestTrue(TEXT("[N] Inside with Y+ as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, 2, 0};
TestFalse(TEXT("[N] Intersecting in Y+"), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, 4, 0};
TestFalse(TEXT("[N] Outside with Y+ as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
// Negative Y
Door.Position = {0, -1, 0};
TestTrue(TEXT("[N] Inside with Y- as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, -2, 0};
TestFalse(TEXT("[N] Intersecting in Y-"), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
Door.Position = {0, -4, 0};
TestFalse(TEXT("[N] Outside with Y- as coincident face."), RoomData->IsRoomInBounds(DungeonBounds, 0, Door));
}
}
// Test HasDoorOfType and variants
{
CREATE_ROOM_DATA(RoomData);
CREATE_DATA_ASSET(UDoorType, DoorA);
CREATE_DATA_ASSET(UDoorType, DoorB);
CREATE_DATA_ASSET(UDoorType, DoorC);
CREATE_DATA_ASSET(UDoorType, DoorD);
CREATE_DATA_ASSET(UDoorType, DoorE);
ADD_DOOR(RoomData, FIntVector::ZeroValue, EDoorDirection::North, DoorA.Get());
ADD_DOOR(RoomData, FIntVector::ZeroValue, EDoorDirection::South, DoorB.Get());
ADD_DOOR(RoomData, FIntVector::ZeroValue, EDoorDirection::East, DoorC.Get());
ADD_DOOR(RoomData, FIntVector::ZeroValue, EDoorDirection::West, DoorD.Get());
// HasDoorOfType
{
TestTrue("HasDoorOfType(DoorA)", RoomData->HasDoorOfType(DoorA.Get()));
TestTrue("HasDoorOfType(DoorB)", RoomData->HasDoorOfType(DoorB.Get()));
TestTrue("HasDoorOfType(DoorC)", RoomData->HasDoorOfType(DoorC.Get()));
TestTrue("HasDoorOfType(DoorD)", RoomData->HasDoorOfType(DoorD.Get()));
TestFalse("HasDoorOfType(DoorE)", RoomData->HasDoorOfType(DoorE.Get()));
TestFalse("HasDoorOfType(nullptr)", RoomData->HasDoorOfType(nullptr));
}
// HasAnyDoorOfType
{
TestTrue("HasAnyDoorOfType({DoorA, DoorB, DoorE, nullptr})", RoomData->HasAnyDoorOfType({DoorA.Get(), DoorB.Get(), DoorE.Get(), nullptr}));
TestTrue("HasAnyDoorOfType({DoorC, DoorD})", RoomData->HasAnyDoorOfType({DoorC.Get(), DoorD.Get()}));
TestFalse("HasAnyDoorOfType({DoorE, nullptr})", RoomData->HasAnyDoorOfType({DoorE.Get(), nullptr}));
TestFalse("HasAnyDoorOfType({})", RoomData->HasAnyDoorOfType({}));
}
// HasAllDoorOfType
{
TestFalse("HasAllDoorOfType({DoorA, DoorB, DoorE, nullptr})", RoomData->HasAllDoorOfType({DoorA.Get(), DoorB.Get(), DoorE.Get(), nullptr}));
TestTrue("HasAllDoorOfType({DoorC, DoorD})", RoomData->HasAllDoorOfType({DoorC.Get(), DoorD.Get()}));
TestFalse("HasAllDoorOfType({DoorE, nullptr})", RoomData->HasAllDoorOfType({DoorE.Get(), nullptr}));
TestTrue("HasAllDoorOfType({})", RoomData->HasAllDoorOfType({}));
}
}
// Test HasCustomData and variants
{
CREATE_ROOM_DATA(RoomData);
RoomData->CustomData.Add(UCustomDataA::StaticClass());
RoomData->CustomData.Add(UCustomDataB::StaticClass());
// HasCustomData
{
TestTrue("HasCustomData(CustomDataA)", RoomData->HasCustomData(UCustomDataA::StaticClass()));
TestTrue("HasCustomData(CustomDataB)", RoomData->HasCustomData(UCustomDataB::StaticClass()));
TestFalse("HasCustomData(CustomDataC)", RoomData->HasCustomData(UCustomDataC::StaticClass()));
TestFalse("HasCustomData(nullptr)", RoomData->HasCustomData(nullptr));
}
// HasAnyCustomData
{
TestTrue("HasAnyCustomData({CustomDataA, CustomDataB})", RoomData->HasAnyCustomData({UCustomDataA::StaticClass(), UCustomDataB::StaticClass()}));
TestTrue("HasAnyCustomData({CustomDataA, CustomDataC})", RoomData->HasAnyCustomData({UCustomDataA::StaticClass(), UCustomDataC::StaticClass()}));
TestFalse("HasAnyCustomData({nullptr, CustomDataC})", RoomData->HasAnyCustomData({nullptr, UCustomDataC::StaticClass()}));
TestFalse("HasAnyCustomData({})", RoomData->HasAnyCustomData({}));
}
// HasAllCustomData
{
TestTrue("HasAllCustomData({CustomDataA, CustomDataB})", RoomData->HasAllCustomData({UCustomDataA::StaticClass(), UCustomDataB::StaticClass()}));
TestFalse("HasAllCustomData({CustomDataA, CustomDataC})", RoomData->HasAllCustomData({UCustomDataA::StaticClass(), UCustomDataC::StaticClass()}));
TestFalse("HasAllCustomData({nullptr, CustomDataC})", RoomData->HasAllCustomData({nullptr, UCustomDataC::StaticClass()}));
TestTrue("HasAllCustomData({})", RoomData->HasAllCustomData({}));
}
}
// Test Size and Volume
{
// Should have Size=(1,1,1) and Volume=1
CREATE_ROOM_DATA(RoomDataA);
RoomDataA->BoundingBoxes[0].SetMinAndMax(FIntVector(0, 1, 0), FIntVector(1, 0, 1));
// Should have Size=(2,1,1) and Volume=2
CREATE_ROOM_DATA(RoomDataB);
RoomDataB->BoundingBoxes[0].SetMinAndMax(FIntVector(-1, 1, 0), FIntVector(1, 0, 1));
// Should have Size=(2,2,1) and Volume=4
CREATE_ROOM_DATA(RoomDataC);
RoomDataC->BoundingBoxes[0].SetMinAndMax(FIntVector(-1, 1, 0), FIntVector(1, -1, 1));
// Should have Size=(2,2,2) and Volume=8
CREATE_ROOM_DATA(RoomDataD);
RoomDataD->BoundingBoxes[0].SetMinAndMax(FIntVector(-1, 1, -1), FIntVector(1, -1, 1));
// GetSize
{
TestEqual("RoomDataA->GetSize() == {1,1,1}", RoomDataA->GetSize(), FIntVector {1, 1, 1});
TestEqual("RoomDataB->GetSize() == {2,1,1}", RoomDataB->GetSize(), FIntVector {2, 1, 1});
TestEqual("RoomDataC->GetSize() == {2,2,1}", RoomDataC->GetSize(), FIntVector {2, 2, 1});
TestEqual("RoomDataD->GetSize() == {2,2,2}", RoomDataD->GetSize(), FIntVector {2, 2, 2});
}
// GetVolume
{
TestEqual("RoomDataA->GetVolume() == 1", RoomDataA->GetVolume(), 1);
TestEqual("RoomDataB->GetVolume() == 2", RoomDataB->GetVolume(), 2);
TestEqual("RoomDataC->GetVolume() == 4", RoomDataC->GetVolume(), 4);
TestEqual("RoomDataD->GetVolume() == 8", RoomDataD->GetVolume(), 8);
}
}
// Test GetVoxelBounds
{
CREATE_ROOM_DATA(RoomDataA);
RoomDataA->Doors.Add({{0, 0, 0}, EDoorDirection::North});
// Should have one cell at (0,0,0) with a door at (0,0,0)[North]
{
FVoxelBounds ExpectedBounds;
ExpectedBounds.AddCell({0, 0, 0});
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
FVoxelBounds ConvertedBounds = RoomDataA->GetVoxelBounds();
TestEqual("RoomDataA->GetVoxelBounds() == ExpectedBounds", ConvertedBounds, ExpectedBounds);
}
CREATE_ROOM_DATA(RoomDataB);
RoomDataB->BoundingBoxes[0].SetMinAndMax(FIntVector(-1, 0, 0), FIntVector(2, 1, 1));
RoomDataB->Doors.Add({{0, 0, 0}, EDoorDirection::West});
RoomDataB->Doors.Add({{1, 0, 0}, EDoorDirection::North});
// Should have 3 cells at (-1,0,0), (0,0,0), (1,0,0) with doors at (0,0,0)[West] and (1,0,0)[North]
{
FVoxelBounds ExpectedBounds;
ExpectedBounds.AddCell({-1, 0, 0});
ExpectedBounds.AddCell({0, 0, 0});
ExpectedBounds.AddCell({1, 0, 0});
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({-1, 0, 0}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({1, 0, 0}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({1, 0, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({1, 0, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({1, 0, 0}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({1, 0, 0}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
FVoxelBounds ConvertedBounds = RoomDataB->GetVoxelBounds();
TestEqual("RoomDataB->GetVoxelBounds() == ExpectedBounds", ConvertedBounds, ExpectedBounds);
}
CREATE_ROOM_DATA(RoomDataC);
RoomDataC->BoundingBoxes[0].SetMinAndMax(FIntVector(0, 0, -1), FIntVector(1, 1, 2));
RoomDataC->Doors.Add({{0, 0, 0}, EDoorDirection::North});
RoomDataC->Doors.Add({{0, 0, 1}, EDoorDirection::South});
// Should have 3 cells at (0,0,-1), (0,0,0), (0,0,1) with doors at (0,0,0)[North] and (0,0,1)[South]
{
FVoxelBounds ExpectedBounds;
ExpectedBounds.AddCell({0, 0, -1});
ExpectedBounds.AddCell({0, 0, 0});
ExpectedBounds.AddCell({0, 0, 1});
ExpectedBounds.SetCellConnection({0, 0, -1}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, -1}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, -1}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, -1}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, -1}, FVoxelBounds::EDirection::Down, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 0}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::North, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::West, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::South, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door));
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::East, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
ExpectedBounds.SetCellConnection({0, 0, 1}, FVoxelBounds::EDirection::Up, FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall));
FVoxelBounds ConvertedBounds = RoomDataC->GetVoxelBounds();
TestEqual("RoomDataC->GetVoxelBounds() == ExpectedBounds", ConvertedBounds, ExpectedBounds);
}
}
// Test Room Constraints
{
#define CHECK_CONSTRAINTS(DATA) URoomData::DoesPassAllConstraints(nullptr, DATA, FIntVector::ZeroValue, EDoorDirection::North)
TestFalse("null data should fail.", CHECK_CONSTRAINTS(nullptr));
CREATE_DATA_ASSET(UConstraintPass, Pass);
CREATE_DATA_ASSET(UConstraintFail, Fail);
CREATE_ROOM_DATA(RoomDataA);
TestTrue("No constraint should pass.", CHECK_CONSTRAINTS(RoomDataA.Get()));
CREATE_ROOM_DATA(RoomDataB);
RoomDataB->Constraints.Add(Pass.Get());
TestTrue("One passing constraint should pass.", CHECK_CONSTRAINTS(RoomDataB.Get()));
CREATE_ROOM_DATA(RoomDataC);
RoomDataC->Constraints.Add(Fail.Get());
TestFalse("One failing constraint should fail.", CHECK_CONSTRAINTS(RoomDataC.Get()));
CREATE_ROOM_DATA(RoomDataD);
RoomDataD->Constraints.Add(Pass.Get());
RoomDataD->Constraints.Add(Pass.Get());
RoomDataD->Constraints.Add(Pass.Get());
TestTrue("All passing constraint should pass.", CHECK_CONSTRAINTS(RoomDataD.Get()));
CREATE_ROOM_DATA(RoomDataE);
RoomDataE->Constraints.Add(Fail.Get());
RoomDataE->Constraints.Add(Pass.Get());
RoomDataE->Constraints.Add(Pass.Get());
TestFalse("First failing constraint should fail.", CHECK_CONSTRAINTS(RoomDataE.Get()));
CREATE_ROOM_DATA(RoomDataF);
RoomDataF->Constraints.Add(Pass.Get());
RoomDataF->Constraints.Add(Fail.Get());
RoomDataF->Constraints.Add(Pass.Get());
TestFalse("Second failing constraint should fail.", CHECK_CONSTRAINTS(RoomDataF.Get()));
CREATE_ROOM_DATA(RoomDataG);
RoomDataG->Constraints.Add(Pass.Get());
RoomDataG->Constraints.Add(Pass.Get());
RoomDataG->Constraints.Add(Fail.Get());
TestFalse("Third failing constraint should fail.", CHECK_CONSTRAINTS(RoomDataG.Get()));
#undef CHECK_CONSTRAINTS
}
return true;
}
#undef CREATE_DATA_ASSET
#undef CREATE_ROOM_DATA
#undef ADD_DOOR
#endif //WITH_DEV_AUTOMATION_TESTS
@@ -0,0 +1,23 @@
// Copyright Benoit Pelletier 2024 - 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#pragma once
#include "UObject/UObjectGlobals.h"
#include "UObject/Package.h"
#include "Misc/EngineVersionComparison.h"
#include "UObject/StrongObjectPtr.h"
#if UE_VERSION_OLDER_THAN(5, 5, 0)
#define FLAG_APPLICATION_CONTEXT EAutomationTestFlags::ApplicationContextMask
#else
#define FLAG_APPLICATION_CONTEXT EAutomationTestFlags_ApplicationContextMask
#endif
// Utility to create a data asset
#define CREATE_DATA_ASSET(VAR_TYPE, VAR_NAME) \
TStrongObjectPtr<VAR_TYPE> VAR_NAME(NewObject<VAR_TYPE>(GetTransientPackage(), #VAR_NAME))
@@ -0,0 +1,522 @@
// Copyright Benoit Pelletier 2025 All Rights Reserved.
//
// This software is available under different licenses depending on the source from which it was obtained:
// - The Fab EULA (https://fab.com/eula) applies when obtained from the Fab marketplace.
// - The CeCILL-C license (https://cecill.info/licences/Licence_CeCILL-C_V1-en.html) applies when obtained from any other source.
// Please refer to the accompanying LICENSE file for further details.
#include "CoreTypes.h"
#include "Containers/UnrealString.h"
#include "Misc/AutomationTest.h"
#include "ProceduralDungeonTypes.h"
#include "VoxelBounds/VoxelBounds.h"
#include "Tests/Classes/CustomScoreCallbacks.h"
#include "TestUtils.h"
#if WITH_DEV_AUTOMATION_TESTS
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FVoxelBoundsTest, "ProceduralDungeon.Types.VoxelBounds", FLAG_APPLICATION_CONTEXT | EAutomationTestFlags::SmokeFilter)
#define SET_CONNECTION(BOUNDS, CELL, DIR, TYPE) \
BOUNDS.SetCellConnection(FIntVector CELL, FVoxelBounds::EDirection::DIR, FVoxelBoundsConnection(EVoxelBoundsConnectionType::TYPE));
bool FVoxelBoundsTest::RunTest(const FString& Parameters)
{
// Extend Test
{
FVoxelBounds BoundsA;
FVoxelBounds BoundsB;
FVoxelBounds BoundsC;
BoundsA.AddCell(FIntVector(0, -1, 0));
BoundsA.AddCell(FIntVector(1, 1, 2));
BoundsB.AddCell(FIntVector(1, 1, 1));
TestEqual(TEXT("BoundsA == ({0, -1, 0}, {2, 2, 3})"), BoundsA.GetBounds(), FBoxMinAndMax({0, -1, 0}, {2, 2, 3}));
TestEqual(TEXT("BoundsB == ({1, 1, 1}, {2, 2, 2})"), BoundsB.GetBounds(), FBoxMinAndMax({1, 1, 1}, {2, 2, 2}));
TestEqual(TEXT("BoundsC == ({0, 0, 0}, {0, 0, 0})"), BoundsC.GetBounds(), FBoxMinAndMax::Invalid);
}
// Comparison Test
{
FVoxelBounds BoundsA;
FVoxelBounds BoundsB;
FVoxelBounds BoundsC;
FVoxelBounds BoundsD;
FVoxelBounds BoundsE;
BoundsA.AddCell(FIntVector(0, 0, 0));
BoundsA.AddCell(FIntVector(1, 1, 1));
SET_CONNECTION(BoundsA, (0, 0, 0), North, Wall);
SET_CONNECTION(BoundsA, (1, 1, 1), South, Door);
// Setting the cell connections in a different way is intentional here
auto& CellA = BoundsB.AddCell(FIntVector(0, 0, 0));
auto& CellB = BoundsB.AddCell(FIntVector(1, 1, 1));
CellA[static_cast<uint8>(FVoxelBounds::EDirection::North)] = FVoxelBoundsConnection(EVoxelBoundsConnectionType::Wall);
CellB[static_cast<uint8>(FVoxelBounds::EDirection::South)] = FVoxelBoundsConnection(EVoxelBoundsConnectionType::Door);
BoundsC.AddCell(FIntVector(0, 0, 0));
BoundsC.AddCell(FIntVector(1, 1, 1));
SET_CONNECTION(BoundsC, (0, 0, 0), North, Wall);
TestEqual(TEXT("BoundsA == BoundsB"), BoundsA, BoundsB);
TestNotEqual(TEXT("BoundsA != BoundsC"), BoundsA, BoundsC);
TestTrue(TEXT("BoundsD is not valid"), !BoundsD.IsValid());
TestNotEqual(TEXT("BoundsA != BoundsD"), BoundsA, BoundsD);
}
// Transform Test
{
FVoxelBounds Bounds;
Bounds.AddCell(FIntVector(0, 0, 0));
Bounds.AddCell(FIntVector(1, 1, 1));
SET_CONNECTION(Bounds, (0, 0, 0), North, Wall);
SET_CONNECTION(Bounds, (1, 1, 1), South, Door);
// Offset by 0
{
TestEqual(TEXT("Bounds + (0,0,0) == OffsetBounds"), Bounds + FIntVector {0, 0, 0}, Bounds);
}
// Simple addition
{
FVoxelBounds OffsetBounds;
OffsetBounds.AddCell(FIntVector(1, -1, 1));
OffsetBounds.AddCell(FIntVector(2, 0, 2));
SET_CONNECTION(OffsetBounds, (1, -1, 1), North, Wall);
SET_CONNECTION(OffsetBounds, (2, 0, 2), South, Door);
TestEqual(TEXT("Bounds + (1,-1,1) == OffsetBounds"), Bounds + FIntVector {1, -1, 1}, OffsetBounds);
}
// Simple subtraction
{
FVoxelBounds OffsetBounds;
OffsetBounds.AddCell(FIntVector(-1, 1, -1));
OffsetBounds.AddCell(FIntVector(0, 2, 0));
SET_CONNECTION(OffsetBounds, (-1, 1, -1), North, Wall);
SET_CONNECTION(OffsetBounds, (0, 2, 0), South, Door);
TestEqual(TEXT("Bounds - (1,-1,1) == OffsetBounds"), Bounds - FIntVector {1, -1, 1}, OffsetBounds);
}
// North rotation
{
TestEqual(TEXT("Bounds.Rotate(North) == Bounds"), Rotate(Bounds, EDoorDirection::North), Bounds);
}
// East rotation
{
FVoxelBounds RotatedBounds;
RotatedBounds.AddCell(FIntVector(0, 0, 0));
RotatedBounds.AddCell(FIntVector(-1, 1, 1));
SET_CONNECTION(RotatedBounds, (0, 0, 0), East, Wall);
SET_CONNECTION(RotatedBounds, (-1, 1, 1), West, Door);
FVoxelBounds NewBounds = Rotate(Bounds, EDoorDirection::East);
TestEqual(TEXT("Bounds.Rotate(East) == RotatedBounds"), NewBounds, RotatedBounds);
}
// South rotation
{
FVoxelBounds RotatedBounds;
RotatedBounds.AddCell(FIntVector(0, 0, 0));
RotatedBounds.AddCell(FIntVector(-1, -1, 1));
SET_CONNECTION(RotatedBounds, (0, 0, 0), South, Wall);
SET_CONNECTION(RotatedBounds, (-1, -1, 1), North, Door);
TestEqual(TEXT("Bounds.Rotate(South) == RotatedBounds"), Rotate(Bounds, EDoorDirection::South), RotatedBounds);
}
// West rotation
{
FVoxelBounds RotatedBounds;
RotatedBounds.AddCell(FIntVector(0, 0, 0));
RotatedBounds.AddCell(FIntVector(1, -1, 1));
SET_CONNECTION(RotatedBounds, (0, 0, 0), West, Wall);
SET_CONNECTION(RotatedBounds, (1, -1, 1), East, Door);
FVoxelBounds NewBounds = Rotate(Bounds, EDoorDirection::West);
TestEqual(TEXT("Bounds.Rotate(West) == RotatedBounds"), NewBounds, RotatedBounds);
}
}
// Concatenation Test
{
FVoxelBounds BoundsA;
BoundsA.AddCell(FIntVector(0, 0, 0));
SET_CONNECTION(BoundsA, (0, 0, 0), North, Door);
SET_CONNECTION(BoundsA, (0, 0, 0), East, Door);
SET_CONNECTION(BoundsA, (0, 0, 0), South, Door);
SET_CONNECTION(BoundsA, (0, 0, 0), West, Door);
SET_CONNECTION(BoundsA, (0, 0, 0), Up, Wall);
SET_CONNECTION(BoundsA, (0, 0, 0), Down, Wall);
FVoxelBounds BoundsB;
BoundsB.AddCell(FIntVector(1, 0, 0));
SET_CONNECTION(BoundsB, (1, 0, 0), North, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), East, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), South, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), West, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), Up, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), Down, Wall);
FVoxelBounds BoundsC = BoundsA + BoundsB;
FVoxelBounds BoundsD = BoundsB + BoundsA;
TestEqual(TEXT("BoundsC == BoundsD"), BoundsC, BoundsD);
TestEqual(TEXT("BoundsC.GetBounds() == BoundsD.GetBounds()"), BoundsC.GetBounds(), BoundsD.GetBounds());
FVoxelBounds ExpectedBounds;
ExpectedBounds.AddCell(FIntVector(0, 0, 0));
ExpectedBounds.AddCell(FIntVector(1, 0, 0));
SET_CONNECTION(ExpectedBounds, (0, 0, 0), East, Door);
SET_CONNECTION(ExpectedBounds, (0, 0, 0), South, Door);
SET_CONNECTION(ExpectedBounds, (0, 0, 0), West, Door);
SET_CONNECTION(ExpectedBounds, (0, 0, 0), Up, Wall);
SET_CONNECTION(ExpectedBounds, (0, 0, 0), Down, Wall);
SET_CONNECTION(ExpectedBounds, (1, 0, 0), North, Wall);
SET_CONNECTION(ExpectedBounds, (1, 0, 0), East, Wall);
SET_CONNECTION(ExpectedBounds, (1, 0, 0), West, Wall);
SET_CONNECTION(ExpectedBounds, (1, 0, 0), Up, Wall);
SET_CONNECTION(ExpectedBounds, (1, 0, 0), Down, Wall);
TestEqual(TEXT("BoundsC == ExpectedBounds"), BoundsC, ExpectedBounds);
}
// Subtraction test
{
FVoxelBounds BoundsA;
BoundsA.AddCell(FIntVector(0, 0, 0));
SET_CONNECTION(BoundsA, (0, 0, 0), North, Door);
SET_CONNECTION(BoundsA, (0, 0, 0), East, Door);
SET_CONNECTION(BoundsA, (0, 0, 0), South, Door);
SET_CONNECTION(BoundsA, (0, 0, 0), West, Door);
SET_CONNECTION(BoundsA, (0, 0, 0), Up, Wall);
SET_CONNECTION(BoundsA, (0, 0, 0), Down, Wall);
FVoxelBounds BoundsB;
BoundsB.AddCell(FIntVector(1, 0, 0));
SET_CONNECTION(BoundsB, (1, 0, 0), North, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), East, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), South, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), West, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), Up, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), Down, Wall);
FVoxelBounds BoundsC;
BoundsC.AddCell(FIntVector(0, 0, 0));
BoundsC.AddCell(FIntVector(1, 0, 0));
SET_CONNECTION(BoundsC, (0, 0, 0), East, Door);
SET_CONNECTION(BoundsC, (0, 0, 0), South, Door);
SET_CONNECTION(BoundsC, (0, 0, 0), West, Door);
SET_CONNECTION(BoundsC, (0, 0, 0), Up, Wall);
SET_CONNECTION(BoundsC, (0, 0, 0), Down, Wall);
SET_CONNECTION(BoundsC, (1, 0, 0), North, Wall);
SET_CONNECTION(BoundsC, (1, 0, 0), East, Wall);
SET_CONNECTION(BoundsC, (1, 0, 0), West, Wall);
SET_CONNECTION(BoundsC, (1, 0, 0), Up, Wall);
SET_CONNECTION(BoundsC, (1, 0, 0), Down, Wall);
FVoxelBounds ExpectedBoundsCMinusA;
ExpectedBoundsCMinusA.AddCell(FIntVector(1, 0, 0));
SET_CONNECTION(ExpectedBoundsCMinusA, (1, 0, 0), North, Wall);
SET_CONNECTION(ExpectedBoundsCMinusA, (1, 0, 0), East, Wall);
SET_CONNECTION(ExpectedBoundsCMinusA, (1, 0, 0), South, Door);
SET_CONNECTION(ExpectedBoundsCMinusA, (1, 0, 0), West, Wall);
SET_CONNECTION(ExpectedBoundsCMinusA, (1, 0, 0), Up, Wall);
SET_CONNECTION(ExpectedBoundsCMinusA, (1, 0, 0), Down, Wall);
FVoxelBounds ExpectedBoundsCMinusB;
ExpectedBoundsCMinusB.AddCell(FIntVector(0, 0, 0));
SET_CONNECTION(ExpectedBoundsCMinusB, (0, 0, 0), North, Wall);
SET_CONNECTION(ExpectedBoundsCMinusB, (0, 0, 0), East, Door);
SET_CONNECTION(ExpectedBoundsCMinusB, (0, 0, 0), South, Door);
SET_CONNECTION(ExpectedBoundsCMinusB, (0, 0, 0), West, Door);
SET_CONNECTION(ExpectedBoundsCMinusB, (0, 0, 0), Up, Wall);
SET_CONNECTION(ExpectedBoundsCMinusB, (0, 0, 0), Down, Wall);
FVoxelBounds BoundsCMinusA = BoundsC - BoundsA;
FVoxelBounds BoundsCMinusB = BoundsC - BoundsB;
TestEqual(TEXT("BoundsC - BoundsA == ExpectedBoundsCMinusA"), BoundsCMinusA, ExpectedBoundsCMinusA);
TestEqual(TEXT("BoundsC - BoundsB == ExpectedBoundsCMinusB"), BoundsCMinusB, ExpectedBoundsCMinusB);
// Non commutative
FVoxelBounds BoundsAMinusC = BoundsA - BoundsC;
FVoxelBounds BoundsBMinusC = BoundsB - BoundsC;
TestFalse(TEXT("BoundsA - BoundsC is invalid"), BoundsAMinusC.IsValid());
TestFalse(TEXT("BoundsB - BoundsC is invalid"), BoundsBMinusC.IsValid());
}
// DoesFitOutside Test
{
// Bounds of 2x2x2
// +---+-o-+ + +---+
// |100 110| |111|
// +-o-+ + + +-o-+
// |010|
// + +---+ + + +
FVoxelBounds DungeonBounds;
DungeonBounds.AddCell(FIntVector(1, 0, 0));
DungeonBounds.AddCell(FIntVector(0, 1, 0));
DungeonBounds.AddCell(FIntVector(1, 1, 0));
DungeonBounds.AddCell(FIntVector(1, 1, 1));
SET_CONNECTION(DungeonBounds, (1, 0, 0), North, Wall);
SET_CONNECTION(DungeonBounds, (1, 0, 0), South, Door);
SET_CONNECTION(DungeonBounds, (1, 0, 0), West, Wall);
SET_CONNECTION(DungeonBounds, (1, 0, 0), Up, Wall);
SET_CONNECTION(DungeonBounds, (1, 0, 0), Down, Wall);
SET_CONNECTION(DungeonBounds, (0, 1, 0), West, Wall);
SET_CONNECTION(DungeonBounds, (0, 1, 0), East, Door);
SET_CONNECTION(DungeonBounds, (0, 1, 0), South, Wall);
SET_CONNECTION(DungeonBounds, (0, 1, 0), Up, Wall);
SET_CONNECTION(DungeonBounds, (0, 1, 0), Down, Wall);
SET_CONNECTION(DungeonBounds, (1, 1, 0), North, Door);
SET_CONNECTION(DungeonBounds, (1, 1, 0), East, Wall);
SET_CONNECTION(DungeonBounds, (1, 1, 0), Down, Wall);
SET_CONNECTION(DungeonBounds, (1, 1, 1), North, Wall);
SET_CONNECTION(DungeonBounds, (1, 1, 1), East, Wall);
SET_CONNECTION(DungeonBounds, (1, 1, 1), South, Door);
SET_CONNECTION(DungeonBounds, (1, 1, 1), West, Wall);
SET_CONNECTION(DungeonBounds, (1, 1, 1), Up, Wall);
// Bounds that fit in the available space
{
// Bounds that fits perfectly in the available space
// + + +
//
// +-o-+ +
// |000|
// +---+ +
FVoxelBounds BoundsA;
BoundsA.AddCell(FIntVector(0, 0, 0));
SET_CONNECTION(BoundsA, (0, 0, 0), North, Door);
SET_CONNECTION(BoundsA, (0, 0, 0), East, Wall);
SET_CONNECTION(BoundsA, (0, 0, 0), South, Wall);
SET_CONNECTION(BoundsA, (0, 0, 0), West, Wall);
SET_CONNECTION(BoundsA, (0, 0, 0), Up, Wall);
SET_CONNECTION(BoundsA, (0, 0, 0), Down, Wall);
// Like BoundsA but with a door facing a wall
// + + +
//
// +-o-+ +
// |000o
// +---+ +
FVoxelBounds BoundsB;
BoundsB.AddCell(FIntVector(0, 0, 0));
SET_CONNECTION(BoundsB, (0, 0, 0), North, Door);
SET_CONNECTION(BoundsB, (0, 0, 0), East, Door);
SET_CONNECTION(BoundsB, (0, 0, 0), South, Wall);
SET_CONNECTION(BoundsB, (0, 0, 0), West, Wall);
SET_CONNECTION(BoundsB, (0, 0, 0), Up, Wall);
SET_CONNECTION(BoundsB, (0, 0, 0), Down, Wall);
// Bounds bigger that fits perfectly in the available space
// +-o-+ +
// |101|
// + + +
// |001o
// +---+ +
FVoxelBounds BoundsC;
BoundsC.AddCell(FIntVector(0, 0, 1));
BoundsC.AddCell(FIntVector(1, 0, 1));
SET_CONNECTION(BoundsC, (0, 0, 1), East, Door);
SET_CONNECTION(BoundsC, (0, 0, 1), South, Wall);
SET_CONNECTION(BoundsC, (0, 0, 1), West, Wall);
SET_CONNECTION(BoundsC, (0, 0, 1), Up, Wall);
SET_CONNECTION(BoundsC, (0, 0, 1), Down, Wall);
SET_CONNECTION(BoundsC, (1, 0, 1), North, Door);
SET_CONNECTION(BoundsC, (1, 0, 1), West, Wall);
SET_CONNECTION(BoundsC, (1, 0, 1), East, Wall);
SET_CONNECTION(BoundsC, (1, 0, 1), Up, Wall);
SET_CONNECTION(BoundsC, (1, 0, 1), Down, Wall);
// Like BoundsC but with a door facing a wall
// +---+ +
// |101o
// + + +
// |001o
// +---+ +
FVoxelBounds BoundsD;
BoundsD.AddCell(FIntVector(0, 0, 1));
BoundsD.AddCell(FIntVector(1, 0, 1));
SET_CONNECTION(BoundsD, (0, 0, 1), East, Door);
SET_CONNECTION(BoundsD, (0, 0, 1), South, Wall);
SET_CONNECTION(BoundsD, (0, 0, 1), West, Wall);
SET_CONNECTION(BoundsD, (0, 0, 1), Up, Wall);
SET_CONNECTION(BoundsD, (0, 0, 1), Down, Wall);
SET_CONNECTION(BoundsD, (1, 0, 1), North, Wall);
SET_CONNECTION(BoundsD, (1, 0, 1), West, Wall);
SET_CONNECTION(BoundsD, (1, 0, 1), East, Door);
SET_CONNECTION(BoundsD, (1, 0, 1), Up, Wall);
SET_CONNECTION(BoundsD, (1, 0, 1), Down, Wall);
// Bounds outside the dungeon bounds
// + +---+ + +-o-+
// |210| |211|
// + +-o-+ + +---+
FVoxelBounds BoundsE;
BoundsE.AddCell(FIntVector(2, 1, 0));
BoundsE.AddCell(FIntVector(2, 1, 1));
SET_CONNECTION(BoundsE, (2, 1, 0), North, Wall);
SET_CONNECTION(BoundsE, (2, 1, 0), East, Wall);
SET_CONNECTION(BoundsE, (2, 1, 0), South, Door);
SET_CONNECTION(BoundsE, (2, 1, 0), West, Wall);
SET_CONNECTION(BoundsE, (2, 1, 0), Down, Wall);
SET_CONNECTION(BoundsE, (2, 1, 1), North, Door);
SET_CONNECTION(BoundsE, (2, 1, 1), East, Wall);
SET_CONNECTION(BoundsE, (2, 1, 1), South, Wall);
SET_CONNECTION(BoundsE, (2, 1, 1), West, Wall);
SET_CONNECTION(BoundsE, (2, 1, 1), Up, Wall);
// Like BoundsE but with a door facing a wall
// + +---+ + +---+
// |210| |211|
// + +-o-+ + +-o-+
FVoxelBounds BoundsF;
BoundsF.AddCell(FIntVector(2, 1, 0));
BoundsF.AddCell(FIntVector(2, 1, 1));
SET_CONNECTION(BoundsF, (2, 1, 0), North, Wall);
SET_CONNECTION(BoundsF, (2, 1, 0), East, Wall);
SET_CONNECTION(BoundsF, (2, 1, 0), South, Door);
SET_CONNECTION(BoundsF, (2, 1, 0), West, Wall);
SET_CONNECTION(BoundsF, (2, 1, 0), Down, Wall);
SET_CONNECTION(BoundsF, (2, 1, 1), North, Wall);
SET_CONNECTION(BoundsF, (2, 1, 1), East, Wall);
SET_CONNECTION(BoundsF, (2, 1, 1), South, Door);
SET_CONNECTION(BoundsF, (2, 1, 1), West, Wall);
SET_CONNECTION(BoundsF, (2, 1, 1), Up, Wall);
// Bounds bigger with a connected door
// + + +
//
// +-o-+-o-+
// |001 011|
// +---+---+
FVoxelBounds BoundsG;
BoundsG.AddCell(FIntVector(0, 0, 1));
BoundsG.AddCell(FIntVector(0, 1, 1));
SET_CONNECTION(BoundsG, (0, 0, 1), North, Door);
SET_CONNECTION(BoundsG, (0, 0, 1), South, Wall);
SET_CONNECTION(BoundsG, (0, 0, 1), West, Wall);
SET_CONNECTION(BoundsG, (0, 0, 1), Up, Wall);
SET_CONNECTION(BoundsG, (0, 0, 1), Down, Wall);
SET_CONNECTION(BoundsG, (0, 1, 1), North, Door);
SET_CONNECTION(BoundsG, (0, 1, 1), South, Wall);
SET_CONNECTION(BoundsG, (0, 1, 1), East, Wall);
SET_CONNECTION(BoundsG, (0, 1, 1), Up, Wall);
SET_CONNECTION(BoundsG, (0, 1, 1), Down, Wall);
int32 _ScoreA, _ScoreB, _ScoreC, _ScoreD, _ScoreE, _ScoreF, _ScoreG;
TestTrue(TEXT("BoundsA fit in DungeonBounds"), BoundsA.GetCompatibilityScore(DungeonBounds, _ScoreA));
TestTrue(TEXT("BoundsB fit in DungeonBounds"), BoundsB.GetCompatibilityScore(DungeonBounds, _ScoreB));
TestTrue(TEXT("BoundsC fit in DungeonBounds"), BoundsC.GetCompatibilityScore(DungeonBounds, _ScoreC));
TestTrue(TEXT("BoundsD fit in DungeonBounds"), BoundsD.GetCompatibilityScore(DungeonBounds, _ScoreD));
TestTrue(TEXT("BoundsE fit in DungeonBounds"), BoundsE.GetCompatibilityScore(DungeonBounds, _ScoreE));
TestTrue(TEXT("BoundsF fit in DungeonBounds"), BoundsF.GetCompatibilityScore(DungeonBounds, _ScoreF));
TestTrue(TEXT("BoundsG fit in DungeonBounds"), BoundsG.GetCompatibilityScore(DungeonBounds, _ScoreG));
// Order of fitting: E(G) > A > C(F) > B > D
TestTrue(TEXT("BoundsE fit better than BoundsA"), _ScoreE > _ScoreA);
TestTrue(TEXT("BoundsA fit better than BoundsC"), _ScoreA > _ScoreC);
TestTrue(TEXT("BoundsC fit better than BoundsB"), _ScoreC > _ScoreB);
TestTrue(TEXT("BoundsB fit better than BoundsD"), _ScoreB > _ScoreD);
TestTrue(TEXT("BoundsG fit equally as BoundsE"), _ScoreG == _ScoreE);
TestTrue(TEXT("BoundsC fit equally as BoundsF"), _ScoreC == _ScoreF);
}
// Bounds that do not fit in the available space
{
// Bounds that does not fit in the available space
// +---+ +
// |100|
// +-o-+ +
//
// + + +
FVoxelBounds BoundsA;
BoundsA.AddCell(FIntVector(1, 0, 0));
SET_CONNECTION(BoundsA, (1, 0, 0), East, Wall);
SET_CONNECTION(BoundsA, (1, 0, 0), North, Door);
SET_CONNECTION(BoundsA, (1, 0, 0), West, Wall);
SET_CONNECTION(BoundsA, (1, 0, 0), Up, Wall);
SET_CONNECTION(BoundsA, (1, 0, 0), Down, Wall);
int32 ScoreA;
TestFalse(TEXT("BoundsA does not fit in DungeonBounds"), BoundsA.GetCompatibilityScore(DungeonBounds, ScoreA));
// Bigger bounds that does not fit in the available space
// +---+ +
// |100|
// + + +
// |000|
// +-o-+ +
FVoxelBounds BoundsB;
BoundsB.AddCell(FIntVector(0, 0, 0));
BoundsB.AddCell(FIntVector(1, 0, 0));
SET_CONNECTION(BoundsB, (0, 0, 0), North, Wall);
SET_CONNECTION(BoundsB, (0, 0, 0), East, Wall);
SET_CONNECTION(BoundsB, (0, 0, 0), South, Door);
SET_CONNECTION(BoundsB, (0, 0, 0), West, Wall);
SET_CONNECTION(BoundsB, (0, 0, 0), Up, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), North, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), West, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), East, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), Up, Wall);
SET_CONNECTION(BoundsB, (1, 0, 0), Down, Wall);
int32 ScoreB;
TestFalse(TEXT("BoundsB does not fit in DungeonBounds"), BoundsB.GetCompatibilityScore(DungeonBounds, ScoreB));
}
// Custom Score Test
{
UCustomScoreCallback* CustomCallbacks = NewObject<UCustomScoreCallback>();
FScoreCallback ZeroScore;
ZeroScore.BindDynamic(CustomCallbacks, &UCustomScoreCallback::ZeroScore);
FScoreCallback NeverPassScore;
NeverPassScore.BindDynamic(CustomCallbacks, &UCustomScoreCallback::NeverPass);
FVoxelBounds BoundsA;
BoundsA.AddCell(FIntVector(0, 0, 0));
SET_CONNECTION(BoundsA, (0, 0, 0), North, Door);
SET_CONNECTION(BoundsA, (0, 0, 0), East, Wall);
SET_CONNECTION(BoundsA, (0, 0, 0), South, Wall);
SET_CONNECTION(BoundsA, (0, 0, 0), West, Wall);
SET_CONNECTION(BoundsA, (0, 0, 0), Up, Wall);
SET_CONNECTION(BoundsA, (0, 0, 0), Down, Wall);
int32 ScoreA;
TestTrue(TEXT("BoundsA does fit in DungeonBounds with ZeroScore"), BoundsA.GetCompatibilityScore(DungeonBounds, ScoreA, ZeroScore));
TestEqual(TEXT("BoundsA ZeroScore should have score of 0"), ScoreA, 0);
int32 ScoreB;
TestFalse(TEXT("BoundsA does not fit in DungeonBounds ith NeverPass"), BoundsA.GetCompatibilityScore(DungeonBounds, ScoreB, NeverPassScore));
TestNotEqual(TEXT("BoundsA NeverPass should have score different from ZeroScore"), ScoreA, ScoreB);
}
}
return true;
}
#undef SET_CONNECTION
#endif //WITH_DEV_AUTOMATION_TESTS