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,44 @@
// 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 "AssetTypeActions_DoorType.h"
#include "DoorType.h"
#include "Modules/ModuleManager.h"
#include "ProceduralDungeonEditor.h"
#define LOCTEXT_NAMESPACE "ProceduralDungeonEditor"
FAssetTypeActions_DoorType::FAssetTypeActions_DoorType()
{
}
FText FAssetTypeActions_DoorType::GetName() const
{
return NSLOCTEXT("AssetTypeActions", "AssetTypeActions_DoorType", "Door Type");
}
UClass* FAssetTypeActions_DoorType::GetSupportedClass() const
{
return UDoorType::StaticClass();
}
FColor FAssetTypeActions_DoorType::GetTypeColor() const
{
return FColor(255, 50, 128);
}
uint32 FAssetTypeActions_DoorType::GetCategories()
{
return EAssetTypeCategories::None; // Defined in the Factory
}
bool FAssetTypeActions_DoorType::HasActions(const TArray<UObject*>& InObjects) const
{
return false;
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,25 @@
// 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.
#pragma once
#include "CoreMinimal.h"
#include "AssetTypeActions_Base.h"
class FAssetTypeActions_DoorType : public FAssetTypeActions_Base
{
public:
FAssetTypeActions_DoorType();
// ~BEGIN FAssetTypeActions_Base
virtual FText GetName() const override;
virtual UClass* GetSupportedClass() const override;
virtual FColor GetTypeColor() const override;
virtual uint32 GetCategories() override;
virtual bool HasActions(const TArray<UObject*>& InObjects) const override;
// ~END FAssetTypeActions_Base
};
@@ -0,0 +1,44 @@
// 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 "AssetTypeActions_RoomData.h"
#include "RoomData.h"
#include "Modules/ModuleManager.h"
#include "ProceduralDungeonEditor.h"
#define LOCTEXT_NAMESPACE "ProceduralDungeonEditor"
FAssetTypeActions_RoomData::FAssetTypeActions_RoomData()
{
}
FText FAssetTypeActions_RoomData::GetName() const
{
return NSLOCTEXT("AssetTypeActions", "AssetTypeActions_RoomData", "Room Data");
}
UClass* FAssetTypeActions_RoomData::GetSupportedClass() const
{
return URoomData::StaticClass();
}
FColor FAssetTypeActions_RoomData::GetTypeColor() const
{
return FColor(255, 50, 128);
}
uint32 FAssetTypeActions_RoomData::GetCategories()
{
return EAssetTypeCategories::None; // Defined in the Factory
}
bool FAssetTypeActions_RoomData::HasActions(const TArray<UObject*>& InObjects) const
{
return false;
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,25 @@
// 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.
#pragma once
#include "CoreMinimal.h"
#include "AssetTypeActions_Base.h"
class FAssetTypeActions_RoomData : public FAssetTypeActions_Base
{
public:
FAssetTypeActions_RoomData();
// ~BEGIN FAssetTypeActions_Base
virtual FText GetName() const override;
virtual UClass* GetSupportedClass() const override;
virtual FColor GetTypeColor() const override;
virtual uint32 GetCategories() override;
virtual bool HasActions(const TArray<UObject*>& InObjects) const override;
// ~END FAssetTypeActions_Base
};
@@ -0,0 +1,82 @@
// 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 "DoorDefCustomization.h"
#include "ProceduralDungeonTypes.h"
#include "PropertyCustomizationHelpers.h"
#define LOCTEXT_NAMESPACE "FDoorDefCustomization"
TSharedRef<IPropertyTypeCustomization> FDoorDefCustomization::MakeInstance()
{
return MakeShareable(new FDoorDefCustomization());
}
void FDoorDefCustomization::GetSortedChildren(TSharedRef<IPropertyHandle> StructPropertyHandle, TArray<TSharedRef<IPropertyHandle>>& OutChildren)
{
TSharedPtr<IPropertyHandle> PositionProp = StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FDoorDef, Position));
TSharedPtr<IPropertyHandle> DirectionProp = StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FDoorDef, Direction));
TSharedPtr<IPropertyHandle> TypeProp = StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FDoorDef, Type));
// ============= Copied from VectorStructCustomization class (private in engine so can't inherit from it) ============================
// Only replaced the StructPropertyHandle to the PositionProp
static const FName X("X");
static const FName Y("Y");
static const FName Z("Z");
TSharedPtr<IPropertyHandle> VectorChildren[3];
uint32 NumChildren;
PositionProp->GetNumChildren(NumChildren);
for (uint32 ChildIndex = 0; ChildIndex < NumChildren; ++ChildIndex)
{
TSharedRef<IPropertyHandle> ChildHandle = PositionProp->GetChildHandle(ChildIndex).ToSharedRef();
const FName PropertyName = ChildHandle->GetProperty()->GetFName();
if (PropertyName == X)
{
VectorChildren[0] = ChildHandle;
}
else if (PropertyName == Y)
{
VectorChildren[1] = ChildHandle;
}
else
{
check(PropertyName == Z);
VectorChildren[2] = ChildHandle;
}
}
OutChildren.Add(VectorChildren[0].ToSharedRef());
OutChildren.Add(VectorChildren[1].ToSharedRef());
OutChildren.Add(VectorChildren[2].ToSharedRef());
// ================ Add Direction property in the children ===================
OutChildren.Add(DirectionProp.ToSharedRef());
OutChildren.Add(TypeProp.ToSharedRef());
}
TSharedRef<SWidget> FDoorDefCustomization::MakeChildWidget(TSharedRef<IPropertyHandle>& StructurePropertyHandle, TSharedRef<IPropertyHandle>& PropertyHandle)
{
const FFieldClass* PropertyClass = PropertyHandle->GetPropertyClass();
if (PropertyClass == FEnumProperty::StaticClass())
return PropertyHandle->CreatePropertyValueWidget();
if (PropertyClass == FStructProperty::StaticClass())
return PropertyHandle->CreatePropertyValueWidget();
if (PropertyClass == FObjectProperty::StaticClass())
return PropertyHandle->CreatePropertyValueWidget();
return FMathStructCustomization::MakeChildWidget(StructurePropertyHandle, PropertyHandle);
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,24 @@
// 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.
#pragma once
#include "IPropertyTypeCustomization.h"
#include "PropertyHandle.h"
#include "Customizations/MathStructCustomizations.h"
class FDoorDefCustomization : public FMathStructCustomization
{
public:
static TSharedRef<IPropertyTypeCustomization> MakeInstance();
protected:
// ~BEGIN FMathStructCustomization
virtual void GetSortedChildren(TSharedRef<IPropertyHandle> StructPropertyHandle, TArray<TSharedRef<IPropertyHandle>>& OutChildren) override;
virtual TSharedRef<SWidget> MakeChildWidget(TSharedRef<IPropertyHandle>& StructurePropertyHandle, TSharedRef<IPropertyHandle>& PropertyHandle) override;
// ~END FMathStructCustomization
};
@@ -0,0 +1,79 @@
// 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 "Margin3DCustomization.h"
#include "ProceduralDungeonEditorSettings.h"
#include "PropertyCustomizationHelpers.h"
#define LOCTEXT_NAMESPACE "FMargin3DCustomization"
TSharedRef<IPropertyTypeCustomization> FMargin3DCustomization::MakeInstance()
{
return MakeShareable(new FMargin3DCustomization());
}
void FMargin3DCustomization::CustomizeHeader(TSharedRef<IPropertyHandle> StructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils)
{
TSharedPtr<IPropertyHandle> AxisProps[3] = {
StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FMargin3D, XAxis)),
StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FMargin3D, YAxis)),
StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FMargin3D, ZAxis))
};
TSharedPtr<SHorizontalBox> ValueRow;
HeaderRow
.NameContent()
[
StructPropertyHandle->CreatePropertyNameWidget()
]
.ValueContent()[
SAssignNew(ValueRow, SHorizontalBox)
];
for (const auto& AxisProp : AxisProps)
{
auto PositiveProp = AxisProp->GetChildHandle(GET_MEMBER_NAME_CHECKED(FVector2D, X));
auto NegativeProp = AxisProp->GetChildHandle(GET_MEMBER_NAME_CHECKED(FVector2D, Y));
// Axis name
ValueRow->AddSlot()
.AutoWidth()
[
AxisProp->CreatePropertyNameWidget()
];
if (PositiveProp)
{
// Margin along positive axis
ValueRow->AddSlot()
.AutoWidth()
.Padding(3.0f, 0.0f)
[
PositiveProp->CreatePropertyValueWidget()
];
}
if (NegativeProp)
{
// Margin along negative axis
ValueRow->AddSlot()
.AutoWidth()
.Padding(0.0f, 0.0f, 20.0f, 0.0f)
[
NegativeProp->CreatePropertyValueWidget()
];
}
}
}
void FMargin3DCustomization::CustomizeChildren(TSharedRef<IPropertyHandle> StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils)
{
// TODO?
}
#undef LOCTEXT_NAMESPACE
@@ -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 "IPropertyTypeCustomization.h"
#include "PropertyHandle.h"
class FMargin3DCustomization : public IPropertyTypeCustomization
{
public:
static TSharedRef<IPropertyTypeCustomization> MakeInstance();
protected:
//~ BEGIN IPropertyTypeCustomization
virtual void CustomizeHeader(TSharedRef<IPropertyHandle> StructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override;
virtual void CustomizeChildren(TSharedRef<IPropertyHandle> StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override;
//~ END IPropertyTypeCustomization
};
@@ -0,0 +1,319 @@
// 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 "ProceduralDungeonEdMode.h"
#include "ProceduralDungeonEditor.h"
#include "Toolkits/ToolkitManager.h"
#include "EditorModeManager.h"
#include "Engine/LevelScriptBlueprint.h"
#include "ProceduralDungeonEdModeToolkit.h"
#include "ProceduralDungeonEdLog.h"
#include "ProceduralDungeonEditorSettings.h"
#include "ProceduralDungeonEditorObject.h"
#include "Tools/ProceduralDungeonEditorTool_Size.h"
#include "Tools/ProceduralDungeonEditorTool_Door.h"
#include "Room.h"
#include "RoomLevel.h"
#include "RoomData.h"
#define ROUTE_TO_TOOL(FuncCall) ActiveTool ? ActiveTool->FuncCall : FEdMode::FuncCall
const FEditorModeID FProceduralDungeonEdMode::EM_ProceduralDungeon(TEXT("EM_ProceduralDungeon"));
FProceduralDungeonEdMode::FProceduralDungeonEdMode()
: FEdMode()
{
Tools.Add(MakeUnique<FProceduralDungeonEditorTool_Size>(this));
Tools.Add(MakeUnique<FProceduralDungeonEditorTool_Door>(this));
Settings = NewObject<UProceduralDungeonEditorObject>(GetTransientPackage(), TEXT("Editor Settings"), RF_Transactional);
}
void FProceduralDungeonEdMode::AddReferencedObjects(FReferenceCollector& Collector)
{
FEdMode::AddReferencedObjects(Collector);
Collector.AddReferencedObject(Settings);
}
void FProceduralDungeonEdMode::Enter()
{
DungeonEd_LogInfo("Enter Room Editor Mode.");
FEdMode::Enter();
if (!Toolkit.IsValid())
{
Toolkit = MakeShareable(new FProceduralDungeonEdModeToolkit);
Toolkit->Init(Owner->GetToolkitHost());
}
UpdateLevelBlueprint();
// Turn on the flag to force the debug drawings.
ARoomLevel::bIsDungeonEditorMode = true;
}
void FProceduralDungeonEdMode::Exit()
{
RegisterLevelCompilationDelegate(false);
FToolkitManager::Get().CloseToolkit(Toolkit.ToSharedRef());
Toolkit.Reset();
if (ActiveTool)
{
ActiveTool->ExitTool();
ActiveTool = nullptr;
}
CachedLevelInstance.Reset();
CachedLevelBlueprint.Reset();
ARoomLevel::bIsDungeonEditorMode = false;
FEdMode::Exit();
DungeonEd_LogInfo("Exit Room Editor Mode.");
}
void FProceduralDungeonEdMode::Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI)
{
if (ActiveTool)
ActiveTool->Render(View, Viewport, PDI);
FEdMode::Render(View, Viewport, PDI);
}
void FProceduralDungeonEdMode::Tick(FEditorViewportClient* ViewportClient, float DeltaTime)
{
FEdMode::Tick(ViewportClient, DeltaTime);
if (ActiveTool)
ActiveTool->Tick(ViewportClient, DeltaTime);
}
bool FProceduralDungeonEdMode::HandleClick(FEditorViewportClient* InViewportClient, HHitProxy* HitProxy, const FViewportClick& Click)
{
return ROUTE_TO_TOOL(HandleClick(InViewportClient, HitProxy, Click));
}
bool FProceduralDungeonEdMode::InputKey(FEditorViewportClient* ViewportClient, FViewport* Viewport, FKey Key, EInputEvent Event)
{
return ROUTE_TO_TOOL(InputKey(ViewportClient, Viewport, Key, Event));
}
bool FProceduralDungeonEdMode::InputAxis(FEditorViewportClient* InViewportClient, FViewport* Viewport, int32 ControllerId, FKey Key, float Delta, float DeltaTime)
{
return ROUTE_TO_TOOL(InputAxis(InViewportClient, Viewport, ControllerId, Key, Delta, DeltaTime));
}
bool FProceduralDungeonEdMode::InputDelta(FEditorViewportClient* InViewportClient, FViewport* InViewport, FVector& InDrag, FRotator& InRot, FVector& InScale)
{
return ROUTE_TO_TOOL(InputDelta(InViewportClient, InViewport, InDrag, InRot, InScale));
}
bool FProceduralDungeonEdMode::MouseMove(FEditorViewportClient* ViewportClient, FViewport* Viewport, int32 MouseX, int32 MouseY)
{
return ROUTE_TO_TOOL(MouseMove(ViewportClient, Viewport, MouseX, MouseY));
}
bool FProceduralDungeonEdMode::ShowModeWidgets() const
{
return true;
}
bool FProceduralDungeonEdMode::ShouldDrawWidget() const
{
return true;
}
bool FProceduralDungeonEdMode::UsesTransformWidget() const
{
return ROUTE_TO_TOOL(UsesTransformWidget());
}
bool FProceduralDungeonEdMode::UsesTransformWidget(WidgetMode CheckMode) const
{
return ROUTE_TO_TOOL(UsesTransformWidget(CheckMode));
}
FVector FProceduralDungeonEdMode::GetWidgetLocation() const
{
return ROUTE_TO_TOOL(GetWidgetLocation());
}
bool FProceduralDungeonEdMode::GetPivotForOrbit(FVector& OutPivot) const
{
const auto* PluginSettings = GetDefault<UProceduralDungeonEditorSettings>();
if (!PluginSettings->bUseRoomAsOrbitPivot)
return false;
if (!CachedLevelInstance.IsValid())
return false;
URoomData* Data = CachedLevelInstance->Data;
if (!IsValid(Data))
return false;
FBoxCenterAndExtent RoomBounds = Data->GetBounds();
OutPivot = RoomBounds.Center;
return true;
}
bool FProceduralDungeonEdMode::GetCursor(EMouseCursor::Type& OutCursor) const
{
return ROUTE_TO_TOOL(GetCursor(OutCursor));
}
bool FProceduralDungeonEdMode::GetTool(FName ToolName, FProceduralDungeonEditorTool*& OutTool) const
{
for (auto& Tool : Tools)
{
if (Tool.IsValid() && Tool->GetToolName() == ToolName)
{
OutTool = Tool.Get();
return true;
}
}
return false;
}
FProceduralDungeonEditorTool* FProceduralDungeonEdMode::GetActiveTool() const
{
return ActiveTool;
}
void FProceduralDungeonEdMode::SetActiveTool(FName ToolName)
{
if (ActiveTool && ActiveTool->GetToolName() == ToolName)
return;
FProceduralDungeonEditorTool* NewTool = nullptr;
if (!GetTool(ToolName, NewTool))
{
DungeonEd_LogError("Tool '%s' is not a valid tool.", *ToolName.ToString());
return;
}
check(NewTool);
SetActiveTool(NewTool);
}
void FProceduralDungeonEdMode::ResetActiveTool()
{
SetActiveTool(nullptr);
}
void FProceduralDungeonEdMode::SetActiveTool(FProceduralDungeonEditorTool* NewTool)
{
if (ActiveTool)
ActiveTool->ExitTool();
DungeonEd_LogInfo("Set active tool to '%s'.", NewTool ? NewTool->GetToolName() : TEXT("None"));
ActiveTool = NewTool;
if (ActiveTool)
ActiveTool->EnterTool();
}
void FProceduralDungeonEdMode::SetDefaultTool()
{
if (!ActiveTool && IsToolEnabled("Tool_Size"))
SetActiveTool("Tool_Size");
}
bool FProceduralDungeonEdMode::IsToolEnabled(FName ToolName) const
{
auto Level = GetLevel();
return Level.IsValid() && IsValid(Level->Data);
}
ULevelScriptBlueprint* FProceduralDungeonEdMode::GetLevelBlueprint(bool bCreate) const
{
UWorld* World = GetWorld();
check(World);
ULevelScriptBlueprint* LevelBlueprint = World->PersistentLevel->GetLevelScriptBlueprint(/*bDontCreate = */ !bCreate);
return LevelBlueprint;
}
TWeakObjectPtr<ARoomLevel> FProceduralDungeonEdMode::GetLevel() const
{
ULevelScriptBlueprint* LevelBlueprint = GetLevelBlueprint();
if (!IsValid(LevelBlueprint))
return nullptr;
return Cast<ARoomLevel>(LevelBlueprint->GeneratedClass->GetDefaultObject());
}
void FProceduralDungeonEdMode::UpdateLevelBlueprint()
{
ULevelScriptBlueprint* LevelBlueprint = GetLevelBlueprint();
if (CachedLevelBlueprint == LevelBlueprint)
return;
if (CachedLevelBlueprint.IsValid())
RegisterLevelCompilationDelegate(false);
CachedLevelBlueprint = LevelBlueprint;
if (CachedLevelBlueprint.IsValid())
RegisterLevelCompilationDelegate(true);
OnLevelBlueprintCompiled();
}
void FProceduralDungeonEdMode::OnLevelBlueprintCompiled(UBlueprint* Blueprint)
{
CachedLevelInstance = Cast<ARoomLevel>(GetWorld()->GetLevelScriptActor());
auto Level = GetLevel();
DungeonEd_LogInfo("Room Level: %s", *GetNameSafe(Level.Get()));
if (Level.IsValid())
SetDefaultTool();
else
ResetActiveTool();
auto RoomToolkit = (FProceduralDungeonEdModeToolkit*)Toolkit.Get();
check(RoomToolkit);
RoomToolkit->OnLevelChanged();
if (ActiveTool)
ActiveTool->OnLevelChanged(Level.Get());
}
void FProceduralDungeonEdMode::RegisterLevelCompilationDelegate(bool Register)
{
if (!CachedLevelBlueprint.IsValid())
{
DungeonEd_LogWarning("Can't (un)register level blueprint compilation delegate: the level blueprint is invalid.");
return;
}
if (Register)
{
if (LevelBlueprintDelegateHandle.IsValid())
{
DungeonEd_LogWarning("Can't register level blueprint compilation delegate: the delegate is already registered.");
}
else
{
LevelBlueprintDelegateHandle = CachedLevelBlueprint->OnCompiled().AddRaw(this, &FProceduralDungeonEdMode::OnLevelBlueprintCompiled);
DungeonEd_LogInfo("Regitered level blueprint compilation delegate.");
}
}
else
{
if (LevelBlueprintDelegateHandle.IsValid())
{
CachedLevelBlueprint->OnCompiled().Remove(LevelBlueprintDelegateHandle);
LevelBlueprintDelegateHandle.Reset();
DungeonEd_LogInfo("Unregitered level blueprint compilation delegate.");
}
else
{
DungeonEd_LogWarning("Can't unregister level blueprint compilation delegate: the delegate is not registered.");
}
}
}
@@ -0,0 +1,73 @@
// 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.
#pragma once
#include "EdMode.h"
#include "ProceduralDungeonEdTypes.h"
#include "EditorMode/Tools/ProceduralDungeonEditorTool.h"
class ARoomLevel;
class ULevelScriptBlueprint;
class FProceduralDungeonEdMode : public FEdMode
{
public:
const static FEditorModeID EM_ProceduralDungeon;
FProceduralDungeonEdMode();
/** FGCObject interface */
virtual void AddReferencedObjects(FReferenceCollector& Collector) override;
//~ Begin FEdMode Interface
virtual void Enter() override;
virtual void Exit() override;
virtual void Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI) override;
virtual void Tick(FEditorViewportClient* ViewportClient, float DeltaTime) override;
virtual bool HandleClick(FEditorViewportClient* InViewportClient, HHitProxy* HitProxy, const FViewportClick& Click) override;
virtual bool InputKey(FEditorViewportClient* ViewportClient, FViewport* Viewport, FKey Key, EInputEvent Event) override;
virtual bool InputAxis(FEditorViewportClient* InViewportClient, FViewport* Viewport, int32 ControllerId, FKey Key, float Delta, float DeltaTime) override;
virtual bool InputDelta(FEditorViewportClient* InViewportClient, FViewport* InViewport, FVector& InDrag, FRotator& InRot, FVector& InScale) override;
virtual bool MouseMove(FEditorViewportClient* ViewportClient, FViewport* Viewport, int32 x, int32 y) override;
virtual bool ShowModeWidgets() const override;
virtual bool ShouldDrawWidget() const override;
virtual bool UsesTransformWidget() const override;
virtual bool UsesTransformWidget(WidgetMode CheckMode) const;
virtual FVector GetWidgetLocation() const override;
virtual bool AllowWidgetMove() override { return true; }
virtual bool GetPivotForOrbit(FVector& OutPivot) const override;
virtual bool GetCursor(EMouseCursor::Type& OutCursor) const override;
//~ End FEdMode Interface
bool GetTool(FName ToolName, FProceduralDungeonEditorTool*& OutTool) const;
FProceduralDungeonEditorTool* GetActiveTool() const;
void SetActiveTool(FName ToolName);
void ResetActiveTool();
void SetDefaultTool();
bool IsToolEnabled(FName ToolName) const;
ULevelScriptBlueprint* GetLevelBlueprint(bool bCreate = false) const;
TWeakObjectPtr<ARoomLevel> GetLevel() const;
TWeakObjectPtr<ARoomLevel> GetLevelInstance() const { return CachedLevelInstance; }
void UpdateLevelBlueprint();
protected:
void SetActiveTool(FProceduralDungeonEditorTool* NewTool);
void OnLevelBlueprintCompiled(UBlueprint* Blueprint = nullptr);
void RegisterLevelCompilationDelegate(bool Register);
public:
GC_PTR(class UProceduralDungeonEditorObject) Settings { nullptr };
private:
TArray<TUniquePtr<FProceduralDungeonEditorTool>> Tools;
FProceduralDungeonEditorTool* ActiveTool = nullptr;
TWeakObjectPtr<ARoomLevel> CachedLevelInstance = nullptr;
TWeakObjectPtr<ULevelScriptBlueprint> CachedLevelBlueprint = nullptr;
FDelegateHandle LevelBlueprintDelegateHandle;
};
@@ -0,0 +1,143 @@
// 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 "ProceduralDungeonEdModeToolkit.h"
#include "ProceduralDungeonEdMode.h"
#include "SProceduralDungeonEdModeWidget.h"
#include "EditorModeManager.h"
#include "ISinglePropertyView.h"
#include "ProceduralDungeonEditorCommands.h"
#include "ProceduralDungeonEditorObject.h"
#include "ProceduralDungeonEdLog.h"
#include "Tools/ProceduralDungeonEditorTool.h"
#define LOCTEXT_NAMESPACE "ProceduralDungeonEditor"
void FProceduralDungeonEdModeToolkit::Init(const TSharedPtr<class IToolkitHost>& InitToolkitHost)
{
auto NameToCommandMap = FProceduralDungeonEditorCommands::Get().NameToCommandMap;
TSharedRef<FUICommandList> CommandList = GetToolkitCommands();
CommandList->MapAction(NameToCommandMap.FindChecked("Tool_Size"), FUIAction(
FExecuteAction::CreateSP(this, &FProceduralDungeonEdModeToolkit::OnChangeTool, FName("Tool_Size")),
FCanExecuteAction::CreateSP(this, &FProceduralDungeonEdModeToolkit::IsToolEnabled, FName("Tool_Size")),
FIsActionChecked::CreateSP(this, &FProceduralDungeonEdModeToolkit::IsToolActive, FName("Tool_Size")))
);
CommandList->MapAction(NameToCommandMap.FindChecked("Tool_Door"), FUIAction(
FExecuteAction::CreateSP(this, &FProceduralDungeonEdModeToolkit::OnChangeTool, FName("Tool_Door")),
FCanExecuteAction::CreateSP(this, &FProceduralDungeonEdModeToolkit::IsToolEnabled, FName("Tool_Door")),
FIsActionChecked::CreateSP(this, &FProceduralDungeonEdModeToolkit::IsToolActive, FName("Tool_Door")))
);
SAssignNew(EdModeWidget, SProceduralDungeonEdModeWidget, SharedThis(this));
FModeToolkit::Init(InitToolkitHost);
}
void FProceduralDungeonEdModeToolkit::GetToolPaletteNames(TArray<FName>& InPaletteName) const
{
InPaletteName.Add(FName("DefaultPalette"));
}
FText FProceduralDungeonEdModeToolkit::GetToolPaletteDisplayName(FName PaletteName) const
{
if (PaletteName == FName("DefaultPalette"))
{
return LOCTEXT("Mode.Default", "Default");
}
return FText();
}
void FProceduralDungeonEdModeToolkit::BuildToolPalette(FName Palette, FToolBarBuilder& ToolbarBuilder)
{
auto CommandList = FProceduralDungeonEditorCommands::Get();
// DoorType property from Settings
UProceduralDungeonEditorObject* EditorSettings = GetDungeonEditorMode()->Settings;
FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
FSinglePropertyParams Params;
#if !COMPATIBILITY
Params.bHideAssetThumbnail = true;
#endif
Params.NamePlacement = EPropertyNamePlacement::Hidden;
TSharedPtr<ISinglePropertyView> SinglePropView = PropertyEditorModule.CreateSingleProperty(EditorSettings, "DoorType", Params);
TSharedPtr<SWidget> Widget =
SNew(SHorizontalBox)
.IsEnabled(this, &FProceduralDungeonEdModeToolkit::IsDoorTypeEnabled)
+ SHorizontalBox::Slot()
.VAlign(EVerticalAlignment::VAlign_Center)
.AutoWidth()
[
SinglePropView.ToSharedRef()
];
ToolbarBuilder.BeginSection("Default");
ToolbarBuilder.AddToolBarButton(CommandList.SizeTool);
ToolbarBuilder.AddSeparator();
ToolbarBuilder.AddToolBarButton(CommandList.DoorTool);
ToolbarBuilder.AddWidget(Widget.ToSharedRef());
ToolbarBuilder.EndSection();
}
FEdMode* FProceduralDungeonEdModeToolkit::GetEditorMode() const
{
return GLevelEditorModeTools().GetActiveMode(FProceduralDungeonEdMode::EM_ProceduralDungeon);
}
FProceduralDungeonEdMode* FProceduralDungeonEdModeToolkit::GetDungeonEditorMode() const
{
return (FProceduralDungeonEdMode*)GetEditorMode();
}
TSharedPtr<SWidget> FProceduralDungeonEdModeToolkit::GetInlineContent() const
{
return EdModeWidget;
}
void FProceduralDungeonEdModeToolkit::OnChangeTool(FName ToolName) const
{
FProceduralDungeonEdMode* EdMode = GetDungeonEditorMode();
if (!EdMode)
{
DungeonEd_LogError("Editor Mode is invalid.");
return;
}
DungeonEd_LogInfo("Change Tool to '%s'", *ToolName.ToString());
EdMode->SetActiveTool(ToolName);
}
bool FProceduralDungeonEdModeToolkit::IsToolEnabled(FName ToolName) const
{
FProceduralDungeonEdMode* EdMode = GetDungeonEditorMode();
return EdMode && EdMode->IsToolEnabled(ToolName);
}
bool FProceduralDungeonEdModeToolkit::IsToolActive(FName ToolName) const
{
FProceduralDungeonEdMode* EdMode = GetDungeonEditorMode();
if (EdMode)
{
FProceduralDungeonEditorTool* Tool = nullptr;
if (EdMode->GetTool(ToolName, Tool))
return EdMode->GetActiveTool() == Tool;
}
return false;
}
bool FProceduralDungeonEdModeToolkit::IsDoorTypeEnabled() const
{
return IsToolEnabled("Tool_Door");
}
void FProceduralDungeonEdModeToolkit::OnLevelChanged()
{
EdModeWidget->OnLevelChanged();
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,37 @@
// 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.
#pragma once
#include "Toolkits/BaseToolkit.h"
class FProceduralDungeonEdModeToolkit : public FModeToolkit
{
public:
/** FModeToolkit interface */
virtual void Init(const TSharedPtr<class IToolkitHost>& InitToolkitHost) override;
virtual void GetToolPaletteNames(TArray<FName>& InPaletteName) const override;
virtual FText GetToolPaletteDisplayName(FName PaletteName) const override;
virtual void BuildToolPalette(FName Palette, class FToolBarBuilder& ToolbarBuilder) override;
/** IToolkit interface */
virtual FName GetToolkitFName() const override { return FName("ProceduralDungeonEdMode"); }
virtual FText GetBaseToolkitName() const override { return NSLOCTEXT("ProceduralDungeonEdModeToolkit", "DisplayName", "ProceduralDungeonEdMode Tool"); }
virtual FEdMode* GetEditorMode() const override;
virtual TSharedPtr<class SWidget> GetInlineContent() const override;
class FProceduralDungeonEdMode* GetDungeonEditorMode() const;
void OnChangeTool(FName ToolName) const;
bool IsToolEnabled(FName ToolName) const;
bool IsToolActive(FName ToolName) const;
bool IsDoorTypeEnabled() const;
void OnLevelChanged();
private:
TSharedPtr<class SProceduralDungeonEdModeWidget> EdModeWidget;
};
@@ -0,0 +1,36 @@
// 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 "ProceduralDungeonEditorCommands.h"
#include "Tools/ProceduralDungeonEditorTool_Size.h"
#include "Tools/ProceduralDungeonEditorTool_Door.h"
#define LOCTEXT_NAMESPACE "ProceduralDungeonEditorCommands"
FName FProceduralDungeonEditorCommands::ProceduralDungeonContext = TEXT("ProceduralDungeonEditor");
FProceduralDungeonEditorCommands::FProceduralDungeonEditorCommands()
: TCommands<FProceduralDungeonEditorCommands>
(
FProceduralDungeonEditorCommands::ProceduralDungeonContext, // Context name for fast lookup
NSLOCTEXT("Contexts", "ProceduralDungeonEditor", "Procedural Dungeon Editor"), // Localized context name for displaying
NAME_None, // Parent
FName("ProceduralDungeonStyle") // Icon Style Set
)
{
}
void FProceduralDungeonEditorCommands::RegisterCommands()
{
UI_COMMAND(SizeTool, "Size", "Change room size by dragging points in the viewport.", EUserInterfaceActionType::RadioButton, FInputChord());
NameToCommandMap.Add("Tool_Size", SizeTool);
UI_COMMAND(DoorTool, "Door", "Add doors easily by clicking in the viewport.", EUserInterfaceActionType::RadioButton, FInputChord());
NameToCommandMap.Add("Tool_Door", DoorTool);
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,30 @@
// 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.
#pragma once
#include "CoreMinimal.h"
#include "Framework/Commands/Commands.h"
#include "EditorStyleSet.h"
class FProceduralDungeonEditorCommands : public TCommands<FProceduralDungeonEditorCommands>
{
public:
FProceduralDungeonEditorCommands();
virtual void RegisterCommands() override;
public:
static FName ProceduralDungeonContext;
// Tools
TSharedPtr<FUICommandInfo> SizeTool = nullptr;
TSharedPtr<FUICommandInfo> DoorTool = nullptr;
// Map
TMap<FName, TSharedPtr<FUICommandInfo>> NameToCommandMap;
};
@@ -0,0 +1,8 @@
// 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 "ProceduralDungeonEditorObject.h"
@@ -0,0 +1,23 @@
// 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.
#pragma once
#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "UObject/Object.h"
#include "ProceduralDungeonEditorObject.generated.h"
UCLASS(MinimalApi)
class UProceduralDungeonEditorObject : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, Category = "Procedural Dungeon Editor")
class UDoorType* DoorType {nullptr};
};
@@ -0,0 +1,650 @@
// 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 "SProceduralDungeonEdModeWidget.h"
#include "EditorModeManager.h"
#include "EditorStyleSet.h"
#include "ISinglePropertyView.h"
#include "Subsystems/AssetEditorSubsystem.h"
#include "Widgets/Layout/SScrollBox.h"
#include "Widgets/Notifications/SErrorText.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/SOverlay.h"
#include "Widgets/Input/SSpinBox.h"
#include "FileHelpers.h"
#include "Engine/LevelScriptBlueprint.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Misc/EngineVersionComparison.h"
#include "GameFramework/Volume.h"
#include "Builders/CubeBuilder.h"
#include "Engine/Selection.h"
#include "ProceduralDungeonEdLog.h"
#include "ProceduralDungeonEditor.h"
#include "ProceduralDungeonEdMode.h"
#include "ProceduralDungeonEdModeToolkit.h"
#include "ProceduralDungeonEditorSettings.h"
#include "Tools/ProceduralDungeonEditorTool.h"
#include "Room.h" // TODO: remove the need to include Room.h when including RoomLevel.h
#include "RoomLevel.h"
#include "RoomData.h"
#if UE_VERSION_OLDER_THAN(5, 1, 0)
using StyleProvider = FEditorStyle;
#else
using StyleProvider = FAppStyle;
#endif
void SProceduralDungeonEdModeWidget::Construct(const FArguments& InArgs, TSharedRef<FProceduralDungeonEdModeToolkit> InParentToolkit)
{
ParentToolkit = InParentToolkit;
FProceduralDungeonEdMode* EdMode = InParentToolkit->GetDungeonEditorMode();
FText LevelName = FText::FromString(GetNameSafe(EdMode->GetWorld()));
FSlateFontInfo TitleFont = StyleProvider::GetFontStyle("DetailsView.CategoryFontStyle");
TitleFont.Size = 24;
FSlateFontInfo SubTitleFont = StyleProvider::GetFontStyle("DetailsView.CategoryFontStyle");
SubTitleFont.Size = 16;
const UProceduralDungeonEditorSettings* EditorSettings = GetDefault<UProceduralDungeonEditorSettings>();
VolumeMargins = EditorSettings->DefaultMargins;
TSharedPtr<SScrollBox> DataScrollBox = nullptr;
ChildSlot
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.VAlign(VAlign_Top)
.Padding(5.f)
.AutoHeight()
[
SNew(STextBlock)
.Text(LevelName)
.Justification(ETextJustify::Center)
.AutoWrapText(true)
.WrappingPolicy(ETextWrappingPolicy::AllowPerCharacterWrapping)
.Font(TitleFont)
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(5.0f)
[
SAssignNew(LevelPropertyContainer, SBorder)
.Visibility(this, &SProceduralDungeonEdModeWidget::ShowDetails)
.BorderBackgroundColor(FLinearColor(0.0f, 0.0f, 0.0f, 0.0f))
]
+ SVerticalBox::Slot()
.Padding(5.0f)
.AutoHeight()
[
SAssignNew(Error, SErrorText)
]
+ SVerticalBox::Slot()
.Padding(5.0f)
[
SNew(SBorder)
.BorderImage(StyleProvider::GetBrush("DetailsView.CollapsedCategory"))
.BorderBackgroundColor(FLinearColor(0.2f, 0.2f, 0.2f, 1.0f))
.Padding(5.0f)
.Visibility(this, &SProceduralDungeonEdModeWidget::ShowDataDetails)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.VAlign(VAlign_Top)
.AutoHeight()
[
SNew(SOverlay)
+ SOverlay::Slot()
.HAlign(EHorizontalAlignment::HAlign_Fill)
[
SNew(STextBlock)
.Text(this, &SProceduralDungeonEdModeWidget::GetDataAssetName)
.Justification(ETextJustify::Center)
.Font(SubTitleFont)
]
+ SOverlay::Slot()
.HAlign(EHorizontalAlignment::HAlign_Left)
.VAlign(EVerticalAlignment::VAlign_Center)
[
SNew(SButton)
.Text(FText::FromString(TEXT("Edit")))
.OnClicked(this, &SProceduralDungeonEdModeWidget::EditData)
.IsEnabled(this, &SProceduralDungeonEdModeWidget::IsValidRoomData)
.HAlign(EHorizontalAlignment::HAlign_Center)
]
+ SOverlay::Slot()
.HAlign(EHorizontalAlignment::HAlign_Right)
.VAlign(EVerticalAlignment::VAlign_Center)
[
SNew(SButton)
.Text(FText::FromString(TEXT("Save")))
.OnClicked(this, &SProceduralDungeonEdModeWidget::SaveData)
.IsEnabled(this, &SProceduralDungeonEdModeWidget::IsDataDirty)
.ButtonColorAndOpacity(this, &SProceduralDungeonEdModeWidget::GetSaveButtonColor)
.HAlign(EHorizontalAlignment::HAlign_Center)
]
]
+ SVerticalBox::Slot()
.FillHeight(1.0f)
.Padding(0.0f, 5.0f, 0.0f, 0.0f)
[
SAssignNew(DataScrollBox, SScrollBox)
//.IsEnabled(this, &SProceduralDungeonEdModeWidget::MatchingDataLevel)
]
]
]
+ SVerticalBox::Slot()
.Padding(5.0f)
.AutoHeight()
[
SNew(SVerticalBox)
.Visibility(this, &SProceduralDungeonEdModeWidget::ShowDataDetails)
+ SVerticalBox::Slot()
.AutoHeight()
.HAlign(EHorizontalAlignment::HAlign_Center)
[
SNew(STextBlock)
.Text(FText::FromString(TEXT("Utilities")))
.Font(SubTitleFont)
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0.0f, 5.0f, 0.0f, 0.0f)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SButton)
.Text(FText::FromString(TEXT("Update Selected Volumes")))
.IsEnabled_Lambda([this]() { return SelectedVolumeCount > 0; })
.OnClicked(this, &SProceduralDungeonEdModeWidget::UpdateSelectedVolumes)
.ToolTipText(FText::FromString(TEXT("Selected volumes will be positioned and sized on the room's bounds (defined in data asset).\nAn optional margin can be set.")))
]
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(20.0f, 0.0f, 0.0f, 0.0f)
.VAlign(EVerticalAlignment::VAlign_Center)
[
SNew(STextBlock)
.Text(FText::FromString(TEXT("Margins")))
.ToolTipText(FText::FromString(TEXT("The amount (in Unreal Unit) to extend the volumes on each side of the room bounds (can be negative).")))
]
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(10.0f, 0.0f, 5.0f, 0.0f)
.VAlign(EVerticalAlignment::VAlign_Center)
[
SNew(STextBlock)
.Text(FText::FromString(TEXT("X")))
]
+ SHorizontalBox::Slot()
.FillWidth(1)
[
SNew(SSpinBox<float>)
.Value(VolumeMargins.XAxis.X)
.OnValueChanged_Lambda([this](float Value) {VolumeMargins.XAxis.X = Value; })
]
+ SHorizontalBox::Slot()
.FillWidth(1)
[
SNew(SSpinBox<float>)
.Value(VolumeMargins.XAxis.Y)
.OnValueChanged_Lambda([this](float Value) {VolumeMargins.XAxis.Y = Value; })
]
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(10.0f, 0.0f, 5.0f, 0.0f)
.VAlign(EVerticalAlignment::VAlign_Center)
[
SNew(STextBlock)
.Text(FText::FromString(TEXT("Y")))
]
+ SHorizontalBox::Slot()
.FillWidth(1)
[
SNew(SSpinBox<float>)
.Value(VolumeMargins.YAxis.X)
.OnValueChanged_Lambda([this](float Value) {VolumeMargins.YAxis.X = Value; })
]
+ SHorizontalBox::Slot()
.FillWidth(1)
[
SNew(SSpinBox<float>)
.Value(VolumeMargins.YAxis.Y)
.OnValueChanged_Lambda([this](float Value) {VolumeMargins.YAxis.Y = Value; })
]
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(10.0f, 0.0f, 5.0f, 0.0f)
.VAlign(EVerticalAlignment::VAlign_Center)
[
SNew(STextBlock)
.Text(FText::FromString(TEXT("Z")))
]
+ SHorizontalBox::Slot()
.FillWidth(1)
[
SNew(SSpinBox<float>)
.Value(VolumeMargins.ZAxis.X)
.OnValueChanged_Lambda([this](float Value) {VolumeMargins.ZAxis.X = Value; })
]
+ SHorizontalBox::Slot()
.FillWidth(1)
[
SNew(SSpinBox<float>)
.Value(VolumeMargins.ZAxis.Y)
.OnValueChanged_Lambda([this](float Value) {VolumeMargins.ZAxis.Y = Value; })
]
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0.0f, 5.0f, 0.0f, 0.0f)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SButton)
.Text(FText::FromString(TEXT("Remove All Invalid Doors")))
//.IsEnabled_Lambda([this]() { return SelectedVolumeCount > 0; })
.OnClicked(this, &SProceduralDungeonEdModeWidget::RemoveInvalidDoors)
.ToolTipText(FText::FromString(TEXT("All invalid doors (drawn in orange) will be removed.")))
]
]
]
+ SVerticalBox::Slot()
.Padding(5.0f)
.AutoHeight()
[
SNew(SBorder)
.HAlign(EHorizontalAlignment::HAlign_Center)
.Visibility(this, &SProceduralDungeonEdModeWidget::ShowNote)
.BorderBackgroundColor(FLinearColor(0.0f, 0.0f, 0.0f, 0.0f))
.Padding(0.0f)
[
SNew(SButton)
.Text(FText::FromString(TEXT("Reparent Level Blueprint")))
.OnClicked(this, &SProceduralDungeonEdModeWidget::ReparentLevelActor)
.ButtonColorAndOpacity(this, &SProceduralDungeonEdModeWidget::GetReparentButtonColor)
.HAlign(EHorizontalAlignment::HAlign_Center)
]
]
];
FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
// RoomData details view
FDetailsViewArgs DetailsViewArgs;
DetailsViewArgs.bAllowSearch = false;
DetailsViewArgs.bLockable = false;
DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea;
DataContentWidget = PropertyEditorModule.CreateDetailView(DetailsViewArgs);
DataContentWidget->OnFinishedChangingProperties().AddLambda([this](const FPropertyChangedEvent& Event) { UpdateErrorText(); });
DataScrollBox->AddSlot()
[
DataContentWidget.ToSharedRef()
];
OnLevelChanged();
RegisterSelectionDelegate(true);
OnSelectedActorsChanged(nullptr);
}
SProceduralDungeonEdModeWidget::~SProceduralDungeonEdModeWidget()
{
RegisterSelectionDelegate(false);
ResetCachedData();
ResetCachedLevel();
}
void SProceduralDungeonEdModeWidget::OnLevelChanged()
{
FProceduralDungeonEdMode* EdMode = GetEditorMode();
ResetCachedLevel();
if (!IsValidRoomLevel(EdMode, &CachedLevel))
{
UpdateErrorText();
return;
}
DungeonEd_LogInfo("Slate Editor Level: %s", *GetNameSafe(CachedLevel.Get()));
LevelDelegateHandle = CachedLevel->OnPropertiesChanged.AddLambda([this](ARoomLevel* RoomLevel) { OnDataAssetChanged(); });
// RoomLevel Data property
FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
TSharedPtr<ISinglePropertyView> SinglePropView = PropertyEditorModule.CreateSingleProperty(CachedLevel.Get(), "Data", {});
LevelPropertyContainer->SetContent(SinglePropView.ToSharedRef());
OnDataAssetChanged();
}
void SProceduralDungeonEdModeWidget::OnDataAssetChanged()
{
auto EdMode = GetEditorMode();
ResetCachedData();
if (IsValidRoomData(EdMode, &CachedData))
{
DataContentWidget->SetObject(CachedData.Get());
if (CachedData->Level.IsNull())
{
CachedData->Modify();
CachedData->Level = EdMode->GetWorld();
DungeonEd_LogInfo("Room Data's Level asset filled with current editor's Level.");
}
DataDelegateHandle = CachedData->OnPropertiesChanged.AddLambda([this](URoomData* Data)
{
UpdateErrorText();
auto EdMode = GetEditorMode();
if (EdMode)
{
FProceduralDungeonEditorTool* ActiveTool = EdMode->GetActiveTool();
if (ActiveTool)
ActiveTool->OnDataPropertiesChanged(CachedData.Get());
}
});
}
UpdateErrorText();
EdMode->SetDefaultTool();
FProceduralDungeonEditorTool* ActiveTool = EdMode->GetActiveTool();
if (ActiveTool)
ActiveTool->OnDataChanged(CachedData.Get());
}
FReply SProceduralDungeonEdModeWidget::ReparentLevelActor()
{
auto EdMode = GetEditorMode();
auto World = EdMode->GetWorld();
ULevelScriptBlueprint* LevelBlueprint = World->PersistentLevel->GetLevelScriptBlueprint();
if (!IsValid(LevelBlueprint))
{
DungeonEd_LogError("ERROR: Can't Reparent Level Blueprint for an unknown reason.");
return FReply::Unhandled();
}
LevelBlueprint->ParentClass = ARoomLevel::StaticClass();
FKismetEditorUtilities::CompileBlueprint(LevelBlueprint);
DungeonEd_LogInfo("Level Blueprint '%s' successfully reparented!", *LevelBlueprint->GetName());
EdMode->UpdateLevelBlueprint();
return FReply::Unhandled();
}
FReply SProceduralDungeonEdModeWidget::EditData()
{
UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
if (!IsValid(AssetEditorSubsystem))
return FReply::Unhandled();
TWeakObjectPtr<URoomData> Data;
if (!IsValidRoomData(nullptr, &Data))
return FReply::Unhandled();
AssetEditorSubsystem->OpenEditorForAsset(Data.Get());
return FReply::Handled();
}
FReply SProceduralDungeonEdModeWidget::SaveData()
{
TWeakObjectPtr<URoomData> Data;
if (!IsValidRoomData(nullptr, &Data))
return FReply::Unhandled();
auto Result = FEditorFileUtils::PromptForCheckoutAndSave({Data->GetPackage()}, /*bCheckDirty = */true, /*bPromptToSave = */false);
if (Result == FEditorFileUtils::EPromptReturnCode::PR_Success)
DungeonEd_LogInfo("Successfully Saved Data Asset: '%s'", *GetNameSafe(Data.Get()));
return FReply::Handled();
}
FReply SProceduralDungeonEdModeWidget::UpdateSelectedVolumes()
{
DungeonEd_LogInfo("Update Selected Volumes.");
auto EdMode = GetEditorMode();
TWeakObjectPtr<ARoomLevel> Level;
TWeakObjectPtr<URoomData> Data;
if (!IsValidRoomData(EdMode, &Data, &Level))
{
DungeonEd_LogError("Can't update selected volumes: RoomData is not valid.");
return FReply::Unhandled();
}
FBoxCenterAndExtent RoomBounds = Data->GetBounds();
RoomBounds = VolumeMargins.Apply(RoomBounds);
GEditor->BeginTransaction(FText::FromString(TEXT("Update Selected Volumes")));
for (auto It = GEditor->GetSelectedActorIterator(); It; ++It)
{
AVolume* Volume = Cast<AVolume>(*It);
if (!IsValid(Volume))
continue;
UCubeBuilder* CubeBrush = Cast<UCubeBuilder>(Volume->BrushBuilder);
if (!IsValid(CubeBrush))
{
DungeonEd_LogWarning("Volume's brush is not a cube. Ignoring this volume.");
continue;
}
DungeonEd_LogInfo("Updating volume: '%s'", *Volume->GetName());
Volume->Modify();
Volume->SetActorLocationAndRotation(RoomBounds.Center, FQuat::Identity);
CubeBrush->Modify();
CubeBrush->X = 2.0f * RoomBounds.Extent.X;
CubeBrush->Y = 2.0f * RoomBounds.Extent.Y;
CubeBrush->Z = 2.0f * RoomBounds.Extent.Z;
// Rebuild volume after changing its builder values
CubeBrush->Build(Volume->GetWorld(), Volume);
}
GEditor->EndTransaction();
return FReply::Handled();
}
FReply SProceduralDungeonEdModeWidget::RemoveInvalidDoors()
{
TWeakObjectPtr<URoomData> Data;
if (!IsValidRoomData(nullptr, &Data))
return FReply::Unhandled();
GEditor->BeginTransaction(FText::FromString(TEXT("Remove Invalid Doors")));
Data->Modify();
for (int i = Data->Doors.Num() - 1; i >= 0; --i)
{
if (!Data->IsDoorValid(i) || Data->IsDoorDuplicate(i))
Data->Doors.RemoveAt(i);
}
GEditor->EndTransaction();
return FReply::Handled();
}
FSlateColor SProceduralDungeonEdModeWidget::GetSaveButtonColor() const
{
const FLinearColor& Default = FLinearColor::White;
const FLinearColor& Highlight = FLinearColor::Green;
return IsDataDirty() ? GetHighlightButtonColor(Highlight, Default) : Default;
}
FSlateColor SProceduralDungeonEdModeWidget::GetReparentButtonColor() const
{
return GetHighlightButtonColor(FLinearColor::Green);
}
void SProceduralDungeonEdModeWidget::UpdateErrorText()
{
auto EdMode = GetEditorMode();
checkf(EdMode, TEXT("EdMode is Invalid in UpdateErrorText"));
if (!IsValidRoomLevel(EdMode))
Error->SetError(TEXT("Persistent Level is not a Room Level."));
else if (!IsValidRoomData(EdMode))
Error->SetError(TEXT("Room Level has no Room Data set."));
else if (!MatchingDataLevel(EdMode))
Error->SetError(TEXT("Level's Data and Data's Level do not match."));
else
Error->SetError(FText::GetEmpty());
}
void SProceduralDungeonEdModeWidget::ResetCachedData()
{
if (!CachedData.IsValid())
return;
CachedData->OnPropertiesChanged.Remove(DataDelegateHandle);
DataDelegateHandle.Reset();
CachedData.Reset();
}
void SProceduralDungeonEdModeWidget::ResetCachedLevel()
{
if (!CachedLevel.IsValid())
return;
CachedLevel->OnPropertiesChanged.Remove(LevelDelegateHandle);
LevelDelegateHandle.Reset();
CachedLevel.Reset();
}
FProceduralDungeonEdMode* SProceduralDungeonEdModeWidget::GetEditorMode() const
{
checkf(ParentToolkit.IsValid(), TEXT("ParentToolkit is invalid. This should never happen. There is a leakage somewhere."));
return ParentToolkit.Pin()->GetDungeonEditorMode();
}
void SProceduralDungeonEdModeWidget::RegisterSelectionDelegate(bool Register)
{
USelection* SelectedActors = GEditor->GetSelectedActors();
checkf(IsValid(SelectedActors), TEXT("Editor Actor Selection is not valid!"));
if (Register)
{
if (SelectionDelegateHandle.IsValid())
{
DungeonEd_LogWarning("Can't register SelectionChanged callback: callback is already registered.");
}
else
{
DungeonEd_LogInfo("Register SelectionChanged callback.");
SelectionDelegateHandle = SelectedActors->SelectionChangedEvent.AddRaw(this, &SProceduralDungeonEdModeWidget::OnSelectedActorsChanged);
}
}
else
{
if (SelectionDelegateHandle.IsValid())
{
DungeonEd_LogInfo("Unregister SelectionChanged callback.");
SelectedActors->SelectionChangedEvent.Remove(SelectionDelegateHandle);
SelectionDelegateHandle.Reset();
}
else
{
DungeonEd_LogWarning("Can't unregister SelectionChanged callback: callback is not registered.");
}
}
}
void SProceduralDungeonEdModeWidget::OnSelectedActorsChanged(UObject* NewSelectedObject)
{
USelection* SelectedActors = GEditor->GetSelectedActors();
SelectedVolumeCount = SelectedActors->CountSelections<AVolume>();
}
bool SProceduralDungeonEdModeWidget::IsValidRoomLevel(FProceduralDungeonEdMode* EdMode, TWeakObjectPtr<ARoomLevel>* OutLevel) const
{
if (!EdMode)
EdMode = GetEditorMode();
auto Level = EdMode->GetLevel();
if (OutLevel)
*OutLevel = Level;
return Level.IsValid();
}
bool SProceduralDungeonEdModeWidget::IsValidRoomData(FProceduralDungeonEdMode* EdMode, TWeakObjectPtr<URoomData>* OutData, TWeakObjectPtr<ARoomLevel>* OutLevel) const
{
if (!EdMode)
EdMode = GetEditorMode();
TWeakObjectPtr<ARoomLevel> Level;
if (!IsValidRoomLevel(EdMode, &Level))
return false;
if (OutLevel)
*OutLevel = Level;
if (OutData)
*OutData = Level->Data;
return IsValid(Level->Data);
}
bool SProceduralDungeonEdModeWidget::MatchingDataLevel(FProceduralDungeonEdMode* EdMode) const
{
if (!EdMode)
EdMode = GetEditorMode();
TWeakObjectPtr<URoomData> Data;
if (!IsValidRoomData(EdMode, &Data))
return false;
return Data->Level.GetUniqueID() == EdMode->GetWorld()->GetPathName();
}
bool SProceduralDungeonEdModeWidget::IsDataDirty(FProceduralDungeonEdMode* EdMode) const
{
TWeakObjectPtr<URoomData> Data;
if (!IsValidRoomData(EdMode, &Data))
return false;
return Data->GetPackage()->IsDirty();
}
EVisibility SProceduralDungeonEdModeWidget::ShowDetails() const
{
return IsValidRoomLevel() ? EVisibility::Visible : EVisibility::Collapsed;
}
EVisibility SProceduralDungeonEdModeWidget::ShowDataDetails() const
{
return IsValidRoomData() ? EVisibility::Visible : EVisibility::Collapsed;
}
EVisibility SProceduralDungeonEdModeWidget::ShowNote() const
{
return IsValidRoomLevel() ? EVisibility::Collapsed : EVisibility::Visible;
}
FText SProceduralDungeonEdModeWidget::GetDataAssetName() const
{
auto EdMode = GetEditorMode();
TWeakObjectPtr<URoomData> Data;
if (!IsValidRoomData(EdMode, &Data))
return FText::GetEmpty();
FString Dirty = IsDataDirty(EdMode) ? "*" : "";
return FText::FromString(GetNameSafe(Data.Get()) + Dirty);
}
FLinearColor SProceduralDungeonEdModeWidget::GetHighlightButtonColor(const FLinearColor& HighlightColor, const FLinearColor& NormalColor, float Speed)
{
uint32 ticks = FDateTime::Now().GetTicks(); // needs this line to avoid compiler optimization that prevent getting Now() each frame.
float seconds = static_cast<float>(ticks) / ETimespan::TicksPerSecond;
float t = FMath::Clamp(FMath::Cos(Speed * seconds), 0.0f, 1.0f);
return FMath::Lerp(NormalColor, HighlightColor, t);
}
@@ -0,0 +1,74 @@
// 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.
#pragma once
#include "Framework/Application/SlateApplication.h"
#include "ProceduralDungeonEdTypes.h"
class ARoomLevel;
class URoomData;
class FProceduralDungeonEdMode;
class FProceduralDungeonEdModeToolkit;
template <typename> class SSpinBox;
class SProceduralDungeonEdModeWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SProceduralDungeonEdModeWidget) {}
SLATE_END_ARGS();
~SProceduralDungeonEdModeWidget();
void Construct(const FArguments& InArgs, TSharedRef<FProceduralDungeonEdModeToolkit> InParentToolkit);
void OnLevelChanged();
protected:
bool IsValidRoomLevel(FProceduralDungeonEdMode* EdMode = nullptr, TWeakObjectPtr<ARoomLevel>* OutLevel = nullptr) const;
bool IsValidRoomData(FProceduralDungeonEdMode* EdMode, TWeakObjectPtr<URoomData>* OutData = nullptr, TWeakObjectPtr<ARoomLevel>* OutLevel = nullptr) const;
bool IsValidRoomData() const { return IsValidRoomData(nullptr); }
bool MatchingDataLevel(FProceduralDungeonEdMode* EdMode = nullptr) const;
bool IsDataDirty(FProceduralDungeonEdMode* EdMode) const;
bool IsDataDirty() const { return IsDataDirty(nullptr); }
EVisibility ShowDetails() const;
EVisibility ShowDataDetails() const;
EVisibility ShowNote() const;
FText GetDataAssetName() const;
void OnDataAssetChanged();
FReply ReparentLevelActor();
FReply EditData();
FReply SaveData();
FReply UpdateSelectedVolumes();
FReply RemoveInvalidDoors();
FSlateColor GetSaveButtonColor() const;
FSlateColor GetReparentButtonColor() const;
void UpdateErrorText();
void ResetCachedData();
void ResetCachedLevel();
FProceduralDungeonEdMode* GetEditorMode() const;
void RegisterSelectionDelegate(bool Register);
void OnSelectedActorsChanged(UObject* NewSelectedObject);
static FLinearColor GetHighlightButtonColor(const FLinearColor& HighlightColor, const FLinearColor& NormalColor = FLinearColor::White, float Speed = 3.0f);
private:
TSharedPtr<class SErrorText> Error {nullptr};
TSharedPtr<class IDetailsView> DataContentWidget {nullptr};
TWeakPtr<FProceduralDungeonEdModeToolkit> ParentToolkit {nullptr};
FMargin3D VolumeMargins;
TSharedPtr<class SBorder> LevelPropertyContainer {nullptr};
TWeakObjectPtr<URoomData> CachedData {nullptr};
TWeakObjectPtr<ARoomLevel> CachedLevel {nullptr};
FDelegateHandle DataDelegateHandle;
FDelegateHandle LevelDelegateHandle;
FDelegateHandle SelectionDelegateHandle;
int32 SelectedVolumeCount {0};
};
@@ -0,0 +1,20 @@
// 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 "ProceduralDungeonEditorTool.h"
#include "EditorMode/ProceduralDungeonEdMode.h"
#include "RoomLevel.h"
#include "RoomData.h"
URoomData* FProceduralDungeonEditorTool::GetRoomData() const
{
auto Level = EdMode->GetLevel();
if (!Level.IsValid())
return nullptr;
return Level.Get()->Data;
}
@@ -0,0 +1,61 @@
// 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.
#pragma once
#include "CoreMinimal.h"
#include "InputCoreTypes.h"
#include "UObject/GCObject.h"
#include "EdMode.h"
#include "EditorUndoClient.h"
#include "ProceduralDungeonEdTypes.h"
class FProceduralDungeonEdMode;
class FProceduralDungeonEditorTool : public FGCObject, public FSelfRegisteringEditorUndoClient
{
public:
FProceduralDungeonEditorTool(FProceduralDungeonEdMode* InEdMode)
: EdMode(InEdMode)
{
}
virtual const TCHAR* GetToolName() = 0;
virtual FText GetDisplayName() = 0;
virtual FText GetDisplayMessage() = 0;
virtual void EnterTool() = 0;
virtual void ExitTool() = 0;
virtual void Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI) {}
virtual void Tick(FEditorViewportClient* ViewportClient, float DeltaTime) {}
virtual bool MouseMove(FEditorViewportClient* ViewportClient, FViewport* Viewport, int32 MouseX, int32 MouseY) { return false; }
virtual bool HandleClick(FEditorViewportClient* InViewportClient, HHitProxy* HitProxy, const FViewportClick& Click) { return false; }
virtual bool InputKey(FEditorViewportClient* InViewportClient, FViewport* InViewport, FKey InKey, EInputEvent InEvent) { return false; }
virtual bool InputAxis(FEditorViewportClient* InViewportClient, FViewport* Viewport, int32 ControllerId, FKey Key, float Delta, float DeltaTime) { return false; }
virtual bool InputDelta(FEditorViewportClient* InViewportClient, FViewport* InViewport, FVector& InDrag, FRotator& InRot, FVector& InScale) { return false; }
virtual bool UsesTransformWidget() const { return false; }
virtual bool UsesTransformWidget(WidgetMode CheckMode) const { return false; }
virtual FVector GetWidgetLocation() const { return FVector::ZeroVector; }
virtual bool GetCursor(EMouseCursor::Type& OutCursor) const { return false; }
//~ Begin FGCObject Interface
virtual void AddReferencedObjects(FReferenceCollector& Collector) override {}
virtual FString GetReferencerName() const override
{
return TEXT("FProceduralDungeonEditorTool");
}
//~ End FGCObject Interface
virtual void OnLevelChanged(const class ARoomLevel* NewLevel) {}
virtual void OnDataChanged(const class URoomData* NewData) {}
virtual void OnDataPropertiesChanged(const class URoomData* Data) {}
class URoomData* GetRoomData() const;
protected:
FProceduralDungeonEdMode* EdMode = nullptr;
};
@@ -0,0 +1,428 @@
// 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 "ProceduralDungeonEditorTool_Door.h"
#include "Components/BoxComponent.h"
#include "EditorMode/ProceduralDungeonEdMode.h"
#include "EditorMode/ProceduralDungeonEditorObject.h"
#include "ProceduralDungeonEdLog.h"
#include "ProceduralDungeonUtils.h"
#include "Room.h"
#include "RoomLevel.h"
#include "RoomData.h"
#include "Door.h"
bool AreDoorsOverlapping(const FDoorDef& DoorA, const FDoorDef& DoorB, const FVector RoomUnit)
{
// If not in same direction, they will never overlap.
if (DoorA.Direction != DoorB.Direction)
return false;
// If same direction and position, then will always overlap.
if (DoorA.Position == DoorB.Position)
return true;
const FBoxCenterAndExtent DoorBoundsA = DoorA.GetBounds(RoomUnit);
const FBoxCenterAndExtent DoorBoundsB = DoorB.GetBounds(RoomUnit);
return Intersect(DoorBoundsA, DoorBoundsB);
}
bool IsPositionInside(const FDoorDef& Door, const FIntVector& Position, const FVector RoomUnit)
{
// If same position, then always inside.
if (Door.Position == Position)
return true;
const FBoxCenterAndExtent DoorBounds = Door.GetBounds(RoomUnit);
const FVector LocalDoorExtent = DoorBounds.Extent / RoomUnit;
const FVector RelativePosition = (FVector(Position) - (DoorBounds.Center / RoomUnit)).GetAbs();
return RelativePosition.X <= LocalDoorExtent.X && RelativePosition.Y <= LocalDoorExtent.Y && RelativePosition.Z <= LocalDoorExtent.Z;
}
bool IsDoorValid(const URoomData* Data, const FDoorDef& Door)
{
check(IsValid(Data));
for (const FDoorDef& DoorDef : Data->Doors)
{
if (AreDoorsOverlapping(Door, DoorDef, Data->GetRoomUnit()))
return false;
}
return true;
}
void FProceduralDungeonEditorTool_Door::EnterTool()
{
DungeonEd_LogInfo("Enter Door Tool.");
UpdateCollision();
}
void FProceduralDungeonEditorTool_Door::ExitTool()
{
DungeonEd_LogInfo("Exit Door Tool.");
DestroyCollision();
CachedLevel.Reset();
}
namespace
{
void RenderBoundingBox(FPrimitiveDrawInterface* PDI, const FBoxCenterAndExtent& Box, const FColor& Color)
{
const FVector Center = Box.Center;
const FVector Extent = Box.Extent;
const FVector A = Center + FVector(Extent.X, Extent.Y, Extent.Z);
const FVector B = Center + FVector(Extent.X, Extent.Y, -Extent.Z);
const FVector C = Center + FVector(Extent.X, -Extent.Y, Extent.Z);
const FVector D = Center + FVector(Extent.X, -Extent.Y, -Extent.Z);
const FVector E = Center + FVector(-Extent.X, Extent.Y, Extent.Z);
const FVector F = Center + FVector(-Extent.X, Extent.Y, -Extent.Z);
const FVector G = Center + FVector(-Extent.X, -Extent.Y, Extent.Z);
const FVector H = Center + FVector(-Extent.X, -Extent.Y, -Extent.Z);
PDI->DrawLine(A, B, Color, SDPG_Foreground);
PDI->DrawLine(A, C, Color, SDPG_Foreground);
PDI->DrawLine(A, E, Color, SDPG_Foreground);
PDI->DrawLine(B, D, Color, SDPG_Foreground);
PDI->DrawLine(B, F, Color, SDPG_Foreground);
PDI->DrawLine(C, D, Color, SDPG_Foreground);
PDI->DrawLine(C, G, Color, SDPG_Foreground);
PDI->DrawLine(D, H, Color, SDPG_Foreground);
PDI->DrawLine(E, F, Color, SDPG_Foreground);
PDI->DrawLine(E, G, Color, SDPG_Foreground);
PDI->DrawLine(F, H, Color, SDPG_Foreground);
PDI->DrawLine(G, H, Color, SDPG_Foreground);
}
void RenderInterlinesBoundingBox(FPrimitiveDrawInterface* PDI, const FBoxMinAndMax& Box, const FColor& Color, const FVector& RoomUnit)
{
FIntVector Min, Max;
IntVector::MinMax(Box.GetMin(), Box.GetMax(), Min, Max);
// Vertical Lines on X
for (int32 i = Min.X + 1; i < Max.X; ++i)
{
FIntVector BottomA(i, Min.Y, Min.Z);
FIntVector BottomB(i, Max.Y, Min.Z);
FIntVector TopA(i, Min.Y, Max.Z);
FIntVector TopB(i, Max.Y, Max.Z);
PDI->DrawLine(Dungeon::ToWorldLocation(BottomA, RoomUnit), Dungeon::ToWorldLocation(TopA, RoomUnit), Color, SDPG_World);
PDI->DrawLine(Dungeon::ToWorldLocation(BottomB, RoomUnit), Dungeon::ToWorldLocation(TopB, RoomUnit), Color, SDPG_World);
}
// Vertical Lines on Y
for (int32 i = Min.Y + 1; i < Max.Y; ++i)
{
FIntVector BottomA(Min.X, i, Min.Z);
FIntVector BottomB(Max.X, i, Min.Z);
FIntVector TopA(Min.X, i, Max.Z);
FIntVector TopB(Max.X, i, Max.Z);
PDI->DrawLine(Dungeon::ToWorldLocation(BottomA, RoomUnit), Dungeon::ToWorldLocation(TopA, RoomUnit), Color, SDPG_World);
PDI->DrawLine(Dungeon::ToWorldLocation(BottomB, RoomUnit), Dungeon::ToWorldLocation(TopB, RoomUnit), Color, SDPG_World);
}
// Horizontal Lines on X and Y
for (int32 i = Min.Z + 1; i < Max.Z; ++i)
{
FIntVector A(Min.X, Min.Y, i);
FIntVector B(Min.X, Max.Y, i);
FIntVector C(Max.X, Max.Y, i);
FIntVector D(Max.X, Min.Y, i);
PDI->DrawLine(Dungeon::ToWorldLocation(A, RoomUnit), Dungeon::ToWorldLocation(B, RoomUnit), Color, SDPG_World);
PDI->DrawLine(Dungeon::ToWorldLocation(B, RoomUnit), Dungeon::ToWorldLocation(C, RoomUnit), Color, SDPG_World);
PDI->DrawLine(Dungeon::ToWorldLocation(C, RoomUnit), Dungeon::ToWorldLocation(D, RoomUnit), Color, SDPG_World);
PDI->DrawLine(Dungeon::ToWorldLocation(D, RoomUnit), Dungeon::ToWorldLocation(A, RoomUnit), Color, SDPG_World);
}
}
}
void FProceduralDungeonEditorTool_Door::Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI)
{
FProceduralDungeonEditorTool::Render(View, Viewport, PDI);
const URoomData* Data = GetRoomData();
if (!IsValid(Data))
return;
const FColor LineColor(100, 20, 0);
const FVector RoomUnit = Data->GetRoomUnit();
for (const auto& BoundingBox : Data->BoundingBoxes)
{
RenderInterlinesBoundingBox(PDI, BoundingBox, LineColor, RoomUnit);
}
}
void FProceduralDungeonEditorTool_Door::Tick(FEditorViewportClient* ViewportClient, float DeltaTime)
{
FProceduralDungeonEditorTool::Tick(ViewportClient, DeltaTime);
if (ShowDoorPreview)
{
const URoomData* Data = GetRoomData();
check(IsValid(Data));
UWorld* World = ViewportClient->GetWorld();
FDoorDef::DrawDebug(World, DoorPreview, Data->GetRoomUnit(), FTransform::Identity, /*includeOffset = */ true, /*isConnected = */ IsDoorValid(Data, DoorPreview));
}
}
bool FProceduralDungeonEditorTool_Door::HandleClick(FEditorViewportClient* InViewportClient, HHitProxy* HitProxy, const FViewportClick& Click)
{
if (Click.IsAltDown() || Click.IsControlDown() || Click.IsShiftDown())
return false;
if (!ShowDoorPreview)
return false;
URoomData* Data = GetRoomData();
check(IsValid(Data));
if (Click.GetKey() == EKeys::LeftMouseButton)
{
if (IsDoorValid(Data, DoorPreview))
{
GEditor->BeginTransaction(FText::FromString(TEXT("Add Door")));
Data->Modify();
Data->Doors.Add(DoorPreview);
GEditor->EndTransaction();
return true;
}
}
if (Click.GetKey() == EKeys::RightMouseButton)
{
if (Data->Doors.Contains(DoorPreview))
{
GEditor->BeginTransaction(FText::FromString(TEXT("Remove Door")));
Data->Modify();
Data->Doors.Remove(DoorPreview);
GEditor->EndTransaction();
return true;
}
}
return false;
}
bool FProceduralDungeonEditorTool_Door::MouseMove(FEditorViewportClient* ViewportClient, FViewport* Viewport, int32 MouseX, int32 MouseY)
{
ShowDoorPreview = false;
const URoomData* Data = GetRoomData();
if (!IsValid(Data))
return false;
FHitResult Hit;
if (!RoomTraceFromMouse(Hit, ViewportClient))
return false;
FIntVector RoomCell;
EDoorDirection DoorDirection;
if (!GetRoomCellFromHit(Hit, Data->GetRoomUnit(), RoomCell, DoorDirection))
return false;
ShowDoorPreview = true;
DoorPreview.Position = RoomCell;
DoorPreview.Direction = DoorDirection;
DoorPreview.Type = EdMode->Settings->DoorType;
// Snap preview to existing door if RoomCell is inside
for (const FDoorDef& RoomDoor : Data->Doors)
{
if (RoomDoor.Direction != DoorPreview.Direction)
continue;
if (IsPositionInside(RoomDoor, DoorPreview.Position, Data->GetRoomUnit()))
{
DoorPreview = RoomDoor;
break;
}
}
return false;
}
bool FProceduralDungeonEditorTool_Door::GetCursor(EMouseCursor::Type& OutCursor) const
{
if (!ShowDoorPreview)
return false;
const URoomData* Data = GetRoomData();
if (!IsValid(Data))
return false;
OutCursor = IsDoorValid(Data, DoorPreview) ? EMouseCursor::Hand : EMouseCursor::SlashedCircle;
return true;
}
void FProceduralDungeonEditorTool_Door::OnLevelChanged(const ARoomLevel* NewLevel)
{
UpdateCollision();
}
void FProceduralDungeonEditorTool_Door::OnDataChanged(const URoomData* NewData)
{
UpdateCollision();
}
void FProceduralDungeonEditorTool_Door::OnDataPropertiesChanged(const URoomData* Data)
{
UpdateCollision();
}
void FProceduralDungeonEditorTool_Door::CreateCollision(ARoomLevel* Level)
{
if (!IsValid(Level))
return;
if (!IsValid(Level->Data))
return;
check(IsValid(Level->GetWorld()));
const int32 NumBoxes = Level->Data->BoundingBoxes.Num();
const int32 Delta = NumBoxes - RoomBoxes.Num();
// Create new boxes when delta is positive
for (int i = 0; i < Delta; ++i)
{
FName BoxName = FName(*FString::Printf(TEXT("Editor Room Collision %d"), RoomBoxes.Num()));
UBoxComponent* RoomBox = NewObject<UBoxComponent>(Level, BoxName, RF_Transient);
check(IsValid(RoomBox));
RoomBox->SetupAttachment(Level->GetRootComponent());
RoomBox->RegisterComponent();
RoomBox->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
RoomBox->SetCollisionObjectType(ECollisionChannel::ECC_MAX);
RoomBoxes.Add(RoomBox);
DungeonEd_LogInfo("Created RoomBox: %s", *GetNameSafe(RoomBox));
}
// Destroy boxes when delta is negative
for (int i = 0; i > Delta; --i)
{
if (RoomBoxes.Num() == 0)
break;
TWeakObjectPtr<UBoxComponent> RoomBox = RoomBoxes.Pop();
if (!RoomBox.IsValid())
continue;
RoomBox->DestroyComponent();
DungeonEd_LogInfo("Destroyed RoomBox: %s", *GetNameSafe(RoomBox.Get()));
}
}
void FProceduralDungeonEditorTool_Door::UpdateCollision()
{
auto Level = EdMode->GetLevelInstance();
if (Level != CachedLevel)
{
DestroyCollision();
CachedLevel = Level;
}
if (!CachedLevel.IsValid())
return;
URoomData* Data = CachedLevel->Data;
if (!IsValid(Data))
return;
CreateCollision(CachedLevel.Get());
check(Data->BoundingBoxes.Num() == RoomBoxes.Num());
for (int i = 0; i < Data->BoundingBoxes.Num(); ++i)
{
const FBoxMinAndMax& DataBox = Data->BoundingBoxes[i];
UBoxComponent* RoomBox = RoomBoxes[i].Get();
FBoxCenterAndExtent Box = Dungeon::ToWorld(DataBox, Data->GetRoomUnit());
RoomBox->SetRelativeLocation(Box.Center);
RoomBox->SetBoxExtent(Box.Extent);
DungeonEd_LogInfo("Updated RoomBox: %s", *GetNameSafe(RoomBox));
}
}
void FProceduralDungeonEditorTool_Door::DestroyCollision()
{
for (const auto& RoomBox : RoomBoxes)
{
if (!RoomBox.IsValid())
continue;
RoomBox->DestroyComponent();
}
RoomBoxes.Empty();
}
bool FProceduralDungeonEditorTool_Door::RoomTraceFromMouse(FHitResult& OutHit, FEditorViewportClient* ViewportClient) const
{
int32 MouseX = ViewportClient->Viewport->GetMouseX();
int32 MouseY = ViewportClient->Viewport->GetMouseY();
// Compute a world space ray from the screen space mouse coordinates
FSceneViewFamilyContext ViewFamily(
FSceneViewFamilyContext::ConstructionValues(ViewportClient->Viewport, ViewportClient->GetScene(), ViewportClient->EngineShowFlags)
.SetRealtimeUpdate(ViewportClient->IsRealtime()));
FSceneView* View = ViewportClient->CalcSceneView(&ViewFamily);
FViewportCursorLocation MouseViewportRay(View, ViewportClient, MouseX, MouseY);
FVector MouseViewportRayDirection = MouseViewportRay.GetDirection();
FVector Start = MouseViewportRay.GetOrigin();
FVector End = Start + WORLD_MAX * MouseViewportRayDirection;
if (ViewportClient->IsOrtho())
{
Start -= WORLD_MAX * MouseViewportRayDirection;
}
return RoomTrace(OutHit, Start, End);
}
bool FProceduralDungeonEditorTool_Door::RoomTrace(FHitResult& OutHit, const FVector& RayOrigin, const FVector& RayEnd) const
{
OutHit = FHitResult();
FHitResult Hit;
bool bHasHit = false;
for (const auto& RoomBox : RoomBoxes)
{
if (!RoomBox.IsValid())
continue;
const bool bSuccess = RoomBox->LineTraceComponent(Hit, RayOrigin, RayEnd, FCollisionQueryParams(SCENE_QUERY_STAT(RoomTrace)));
if (!bSuccess)
continue;
if (!bHasHit || Hit.Distance < OutHit.Distance)
{
OutHit = Hit;
bHasHit = true;
}
}
return bHasHit;
}
bool FProceduralDungeonEditorTool_Door::GetRoomCellFromHit(const FHitResult& Hit, const FVector RoomUnit, FIntVector& OutCell, EDoorDirection& OutDirection) const
{
// Direction is up or down: invalid
if (FMath::Abs(FVector::DotProduct(Hit.ImpactNormal, FVector::UpVector)) >= 0.5f)
return false;
// Determine direction from the hit normal
float DirX = FVector::DotProduct(Hit.ImpactNormal, FVector::ForwardVector);
float DirY = FVector::DotProduct(Hit.ImpactNormal, FVector::RightVector);
if (FMath::Abs(DirX) > FMath::Abs(DirY))
OutDirection = (DirX > 0) ? EDoorDirection::North : EDoorDirection::South;
else
OutDirection = (DirY > 0) ? EDoorDirection::East : EDoorDirection::West;
// Determine the room cell
FVector RoomSpacePoint = Hit.ImpactPoint / RoomUnit;
RoomSpacePoint -= 0.5f * (ToVector(OutDirection) + FVector::UpVector);
OutCell = FIntVector(FMath::RoundToInt(RoomSpacePoint.X), FMath::RoundToInt(RoomSpacePoint.Y), FMath::RoundToInt(RoomSpacePoint.Z));
return true;
}
@@ -0,0 +1,53 @@
// 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.
#pragma once
#include "ProceduralDungeonEditorTool.h"
class ARoomLevel;
class FProceduralDungeonEditorTool_Door : public FProceduralDungeonEditorTool
{
public:
FProceduralDungeonEditorTool_Door(FProceduralDungeonEdMode* InEdMode)
: FProceduralDungeonEditorTool(InEdMode)
{
}
//~ Begin FProceduralDungeonEditorTool Interface
virtual const TCHAR* GetToolName() override { return TEXT("Tool_Door"); }
virtual FText GetDisplayName() override { return NSLOCTEXT("ProceduralDungeonEditor", "Tool_Door", "Door"); }
virtual FText GetDisplayMessage() override { return NSLOCTEXT("ProceduralDungeonEditor", "Tool_Door_Tooltip", ""); }
virtual void EnterTool() override;
virtual void ExitTool() override;
virtual void Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI) override;
virtual void Tick(FEditorViewportClient* ViewportClient, float DeltaTime) override;
virtual bool HandleClick(FEditorViewportClient* InViewportClient, HHitProxy* HitProxy, const FViewportClick& Click) override;
virtual bool MouseMove(FEditorViewportClient* ViewportClient, FViewport* Viewport, int32 MouseX, int32 MouseY) override;
virtual bool GetCursor(EMouseCursor::Type& OutCursor) const override;
virtual void OnLevelChanged(const ARoomLevel* NewLevel) override;
virtual void OnDataChanged(const URoomData* NewData) override;
virtual void OnDataPropertiesChanged(const URoomData* Data) override;
//~ End FProceduralDungeonEditorTool Interface
protected:
void CreateCollision(ARoomLevel* level);
void UpdateCollision();
void DestroyCollision();
bool RoomTraceFromMouse(FHitResult& OutHit, FEditorViewportClient* ViewportClient) const;
bool RoomTrace(FHitResult& OutHit, const FVector& RayOrigin, const FVector& RayEnd) const;
bool GetRoomCellFromHit(const FHitResult& Hit, const FVector RoomUnit, FIntVector& OutCell, EDoorDirection& OutDirection) const;
private:
TWeakObjectPtr<ARoomLevel> CachedLevel = nullptr;
TArray<TWeakObjectPtr<class UBoxComponent>> RoomBoxes;
FDoorDef DoorPreview;
bool ShowDoorPreview {false};
};
@@ -0,0 +1,337 @@
// 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 "ProceduralDungeonEditorTool_Size.h"
#include "EditorMode/ProceduralDungeonEdMode.h"
#include "EditorModeManager.h"
#include "Engine/Selection.h"
#include "ProceduralDungeonEdLog.h"
#include "ProceduralDungeonUtils.h"
#include "Room.h"
#include "RoomLevel.h"
#include "RoomData.h"
IMPLEMENT_HIT_PROXY(HRoomPointProxy, HHitProxy);
void FRoomPoint::AddLinkedPoint(FRoomPoint& Point, EAxisList::Type Axis)
{
LinkedPoints.Add({&Point, Axis});
Point.LinkedPoints.Add({this, Axis});
}
void FRoomPoint::SetLocation(FVector InLocation)
{
SetDirty();
Location = InLocation;
UpdateWithPropagation();
}
void FRoomPoint::SetDirty()
{
if (bDirty)
return;
bDirty = EAxisList::XYZ;
for (FLink& Link : LinkedPoints)
{
check(Link.Point);
Link.Point->SetDirty();
}
}
void FRoomPoint::UpdateWithPropagation()
{
TQueue<FRoomPoint*> PendingNodes;
PendingNodes.Enqueue(this);
FRoomPoint* Node = nullptr;
while (PendingNodes.Dequeue(Node))
{
Node->UpdateLinkedPoints(PendingNodes);
}
}
void FRoomPoint::UpdateLinkedPoints(TQueue<FRoomPoint*>& PendingNodes)
{
for (FLink& Link : LinkedPoints)
{
check(Link.Point);
if (Link.Point->bDirty)
{
Link.Point->UpdateFrom(*this, Link.Axis);
PendingNodes.Enqueue(Link.Point);
}
}
}
void FRoomPoint::UpdateFrom(FRoomPoint& From, EAxisList::Type Axis)
{
if (bDirty & Axis & EAxisList::X)
{
bDirty &= ~EAxisList::X;
From.bDirty &= ~EAxisList::X;
Location.X = From.Location.X;
}
if (bDirty & Axis & EAxisList::Y)
{
bDirty &= ~EAxisList::Y;
From.bDirty &= ~EAxisList::Y;
Location.Y = From.Location.Y;
}
if (bDirty & Axis & EAxisList::Z)
{
bDirty &= ~EAxisList::Z;
From.bDirty &= ~EAxisList::Z;
Location.Z = From.Location.Z;
}
}
void FProceduralDungeonEditorTool_Size::EnterTool()
{
OnDataChanged();
ResetSelectedPoint();
}
void FProceduralDungeonEditorTool_Size::ExitTool()
{
DungeonEd_LogInfo("Exit Size Tool.");
}
void FProceduralDungeonEditorTool_Size::Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI)
{
const URoomData* Data = GetRoomData();
if (!IsValid(Data))
return;
for (const FBoxPoints& Box : Boxes)
{
Box.Draw(PDI, SelectedPoint);
}
}
bool FProceduralDungeonEditorTool_Size::HandleClick(FEditorViewportClient* InViewportClient, HHitProxy* HitProxy, const FViewportClick& Click)
{
ResetSelectedPoint();
if (!HitProxy)
return false;
if (!HitProxy->IsA(HRoomPointProxy::StaticGetType()))
return false;
const HRoomPointProxy* RoomProxy = (HRoomPointProxy*)HitProxy;
checkf(RoomProxy->Index >= 0 && RoomProxy->Index < GetMaxIndex(), TEXT("A room point has been clicked but is out of bounds!"));
SetSelectedPoint(RoomProxy->Index);
GEditor->GetSelectedActors()->DeselectAll();
// Force translate widget when a point is selected.
FEditorModeTools* ModeTools = EdMode->GetModeManager();
if (ModeTools)
ModeTools->SetWidgetMode(WidgetMode::WM_Translate);
return true;
}
bool FProceduralDungeonEditorTool_Size::InputKey(FEditorViewportClient* InViewportClient, FViewport* InViewport, FKey InKey, EInputEvent InEvent)
{
if (InKey == EKeys::LeftMouseButton && InEvent == IE_Released)
ResetDragPoint();
return false;
}
bool FProceduralDungeonEditorTool_Size::InputDelta(FEditorViewportClient* InViewportClient, FViewport* InViewport, FVector& InDrag, FRotator& InRot, FVector& InScale)
{
if (InViewportClient->GetCurrentWidgetAxis() == EAxisList::None)
return false;
if (!HasValidSelection())
return false;
const URoomData* Data = GetRoomData();
if (!IsValid(Data))
return false;
if (!InDrag.IsNearlyZero())
{
DragPoint += InDrag;
FRoomPoint& Point = GetSelectedPoint();
FVector OldPoint = Point.GetLocation();
Point.SetLocation(Dungeon::SnapPoint(DragPoint, Data->GetRoomUnit()));
if (OldPoint != Point.GetLocation())
UpdateDataAsset(GetBoxIndexFromPointIndex(SelectedPoint));
}
return true;
}
bool FProceduralDungeonEditorTool_Size::UsesTransformWidget() const
{
return HasValidSelection() || (GEditor->GetSelectedActorCount() > 0);
}
bool FProceduralDungeonEditorTool_Size::UsesTransformWidget(WidgetMode CheckMode) const
{
return HasValidSelection() ? (CheckMode == WidgetMode::WM_Translate) : EdMode->FEdMode::UsesTransformWidget(CheckMode);
}
FVector FProceduralDungeonEditorTool_Size::GetWidgetLocation() const
{
return HasValidSelection() ? DragPoint : EdMode->FEdMode::GetWidgetLocation();
}
void FProceduralDungeonEditorTool_Size::PostUndo(bool bSuccess)
{
OnDataChanged();
}
void FProceduralDungeonEditorTool_Size::PostRedo(bool bSuccess)
{
OnDataChanged();
}
void FProceduralDungeonEditorTool_Size::OnDataChanged(const URoomData* NewData)
{
// When the room data has changed, reset all
ResetSelectedPoint();
ResetDragPoint();
UpdateBoxes(NewData);
}
void FProceduralDungeonEditorTool_Size::OnDataPropertiesChanged(const URoomData* Data)
{
UpdateBoxes(Data);
}
bool FProceduralDungeonEditorTool_Size::HasValidSelection() const
{
return SelectedPoint >= 0 && SelectedPoint < GetMaxIndex();
}
void FProceduralDungeonEditorTool_Size::ResetDragPoint()
{
if (HasValidSelection())
DragPoint = GetSelectedPoint().GetLocation();
else
DragPoint = FVector::ZeroVector;
}
void FProceduralDungeonEditorTool_Size::UpdateDataAsset(int32 BoxIndex) const
{
URoomData* Data = GetRoomData();
if (!IsValid(Data))
return;
if (BoxIndex >= 0 && BoxIndex < Data->BoundingBoxes.Num())
{
Data->Modify();
const FVector RoomUnit = Data->GetRoomUnit();
const FBoxPoints& Box = Boxes[BoxIndex];
FBoxMinAndMax& DataBox = Data->BoundingBoxes[BoxIndex];
const FIntVector A = Dungeon::ToRoomLocation(Box.GetMin(), RoomUnit);
const FIntVector B = Dungeon::ToRoomLocation(Box.GetMax(), RoomUnit);
DataBox.SetMinAndMax(A, B);
return;
}
}
void FProceduralDungeonEditorTool_Size::SetSelectedPoint(int32 Index)
{
if (Index < 0 || Index >= GetMaxIndex())
Index = -1;
DungeonEd_LogInfo("Selected Point: %d", Index);
SelectedPoint = Index;
ResetDragPoint();
}
void FProceduralDungeonEditorTool_Size::ResetSelectedPoint()
{
SetSelectedPoint(-1);
}
FRoomPoint& FProceduralDungeonEditorTool_Size::GetSelectedPoint()
{
checkf(HasValidSelection(), TEXT("Trying to get selected point but selection is invalid!"));
const int32 BoxIndex = GetBoxIndexFromPointIndex(SelectedPoint);
const int32 PointIndex = GetBoxPointIndex(SelectedPoint);
checkf(BoxIndex >= 0 && BoxIndex < Boxes.Num(), TEXT("Selected point's box index is out of bounds!"));
checkf(PointIndex >= 0 && PointIndex < FBoxPoints::NbPoints, TEXT("Selected point's point index is out of bounds!"));
return Boxes[BoxIndex].GetPoint(PointIndex);
}
void FProceduralDungeonEditorTool_Size::UpdateBoxes(const URoomData* Data)
{
if (!IsValid(Data))
return;
const int32 NbBoxes = Data->BoundingBoxes.Num();
if (Boxes.Num() != NbBoxes)
{
Boxes.SetNum(NbBoxes);
}
const FVector RoomUnit = Data->GetRoomUnit();
for (int32 i = 0; i < NbBoxes; ++i)
{
const FBoxMinAndMax& Box = Data->BoundingBoxes[i];
Boxes[i].SetID(i);
Boxes[i].SetMin(Dungeon::ToWorldLocation(Box.GetMin(), RoomUnit));
Boxes[i].SetMax(Dungeon::ToWorldLocation(Box.GetMax(), RoomUnit));
}
}
FBoxPoints::FBoxPoints()
{
// 6 1
// 2 7
// 4 5
// 0 3
Points[0].AddLinkedPoint(Points[2], EAxisList::XY);
Points[0].AddLinkedPoint(Points[3], EAxisList::XZ);
Points[0].AddLinkedPoint(Points[4], EAxisList::YZ);
Points[1].AddLinkedPoint(Points[5], EAxisList::XY);
Points[1].AddLinkedPoint(Points[6], EAxisList::XZ);
Points[1].AddLinkedPoint(Points[7], EAxisList::YZ);
Points[2].AddLinkedPoint(Points[6], EAxisList::YZ);
Points[2].AddLinkedPoint(Points[7], EAxisList::XZ);
Points[3].AddLinkedPoint(Points[5], EAxisList::YZ);
Points[3].AddLinkedPoint(Points[7], EAxisList::XY);
Points[4].AddLinkedPoint(Points[5], EAxisList::XZ);
Points[4].AddLinkedPoint(Points[6], EAxisList::XY);
}
void FBoxPoints::SetMin(FVector Value)
{
Points[0].SetLocation(Value);
}
void FBoxPoints::SetMax(FVector Value)
{
Points[1].SetLocation(Value);
}
void FBoxPoints::Draw(FPrimitiveDrawInterface* PDI, int32 SelectedPoint) const
{
static const FColor NormalColor(200, 200, 200);
static const FColor SelectedColor(255, 128, 0);
const int32 LocalSelectedIndex = SelectedPoint - (ID * NbPoints);
for (int i = 0; i < NbPoints; ++i)
{
const bool bSelected = (LocalSelectedIndex == i);
const FColor& Color = bSelected ? SelectedColor : NormalColor;
PDI->SetHitProxy(new HRoomPointProxy(ID * NbPoints + i));
PDI->DrawPoint(Points[i].GetLocation(), Color, 10.0f, SDPG_Foreground);
PDI->SetHitProxy(nullptr);
}
}
@@ -0,0 +1,126 @@
// 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.
#pragma once
#include "CoreMinimal.h"
#include "ProceduralDungeonEditorTool.h"
struct HRoomPointProxy : public HHitProxy
{
DECLARE_HIT_PROXY();
HRoomPointProxy(int32 InIndex)
: HHitProxy(HPP_UI)
, Index(InIndex)
{
}
int32 Index {-1};
};
struct FRoomPoint
{
struct FLink
{
FRoomPoint* Point;
EAxisList::Type Axis;
};
void AddLinkedPoint(FRoomPoint& Point, EAxisList::Type Axis);
FVector GetLocation() const { return Location; }
void SetLocation(FVector InLocation);
FVector* operator->() { return &Location; }
private:
void SetDirty();
void UpdateWithPropagation();
void UpdateLinkedPoints(TQueue<FRoomPoint*>& PendingNodes);
void UpdateFrom(FRoomPoint& From, EAxisList::Type Axis);
private:
EAxisList::Type bDirty {EAxisList::None};
FVector Location {0};
TArray<FLink> LinkedPoints {};
};
struct FBoxPoints
{
public:
FBoxPoints();
void SetID(uint32 InID) { ID = InID; }
void SetMin(FVector Value);
void SetMax(FVector Value);
uint32 GetID() const { return ID; }
FVector GetMin() const { return Points[0].GetLocation(); }
FVector GetMax() const { return Points[1].GetLocation(); }
FRoomPoint& GetPoint(int32 Index)
{
check(Index >= 0 && Index < NbPoints);
return Points[Index];
}
void Draw(FPrimitiveDrawInterface* PDI, int32 SelectedPoint) const;
static const uint32 NbPoints {8};
private:
uint32 ID;
FRoomPoint Points[NbPoints];
};
class FProceduralDungeonEditorTool_Size : public FProceduralDungeonEditorTool
{
public:
FProceduralDungeonEditorTool_Size(FProceduralDungeonEdMode* InEdMode)
: FProceduralDungeonEditorTool(InEdMode)
{
}
virtual const TCHAR* GetToolName() override { return TEXT("Tool_Size"); }
virtual FText GetDisplayName() override { return NSLOCTEXT("ProceduralDungeonEditor", "Tool_Size", "Size"); }
virtual FText GetDisplayMessage() override { return NSLOCTEXT("ProceduralDungeonEditor", "Tool_Size_Tooltip", ""); }
virtual void EnterTool() override;
virtual void ExitTool() override;
virtual void Render(const FSceneView* View, FViewport* Viewport, FPrimitiveDrawInterface* PDI) override;
virtual bool HandleClick(FEditorViewportClient* InViewportClient, HHitProxy* HitProxy, const FViewportClick& Click) override;
virtual bool InputKey(FEditorViewportClient* InViewportClient, FViewport* InViewport, FKey InKey, EInputEvent InEvent) override;
virtual bool InputDelta(FEditorViewportClient* InViewportClient, FViewport* InViewport, FVector& InDrag, FRotator& InRot, FVector& InScale) override;
virtual bool UsesTransformWidget() const override;
virtual bool UsesTransformWidget(WidgetMode CheckMode) const override;
virtual FVector GetWidgetLocation() const override;
/** FEditorUndoClient interface */
virtual void PostUndo(bool bSuccess) override;
virtual void PostRedo(bool bSuccess) override;
virtual void OnDataChanged(const URoomData* NewData = nullptr) override;
virtual void OnDataPropertiesChanged(const URoomData* Data = nullptr) override;
bool HasValidSelection() const;
void ResetDragPoint();
void UpdateDataAsset(int32 BoxIndex) const;
void SetSelectedPoint(int32 Index);
void ResetSelectedPoint();
private:
int32 GetBoxIndexFromPointIndex(int32 PointIndex) const { return PointIndex / FBoxPoints::NbPoints; }
int32 GetBoxPointIndex(int32 PointIndex) const { return PointIndex % FBoxPoints::NbPoints; }
int32 GetMaxIndex() const { return Boxes.Num() * FBoxPoints::NbPoints; }
FRoomPoint& GetSelectedPoint();
void UpdateBoxes(const URoomData* Data);
private:
int32 SelectedPoint {-1};
TArray<FBoxPoints> Boxes;
FVector DragPoint;
};
@@ -0,0 +1,30 @@
// 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 "DoorTypeFactory.h"
#include "DoorType.h"
#include "AssetTypeCategories.h"
#include "ProceduralDungeonEditor.h"
UDoorTypeFactory::UDoorTypeFactory()
{
bCreateNew = true;
bEditAfterNew = true;
SupportedClass = UDoorType::StaticClass();
}
uint32 UDoorTypeFactory::GetMenuCategories() const
{
FProceduralDungeonEditorModule& EditorModule = FModuleManager::LoadModuleChecked<FProceduralDungeonEditorModule>("ProceduralDungeonEditor");
return EditorModule.GetAssetTypeCategory();
}
UObject* UDoorTypeFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
UDoorType* DoorType = NewObject<UDoorType>(InParent, InClass, InName, Flags);
return DoorType;
}
@@ -0,0 +1,24 @@
// 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.
#pragma once
#include "CoreMinimal.h"
#include "Factories/Factory.h"
#include "DoorTypeFactory.generated.h"
UCLASS()
class UDoorTypeFactory : public UFactory
{
GENERATED_BODY()
public:
UDoorTypeFactory();
virtual uint32 GetMenuCategories() const override;
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn);
};
@@ -0,0 +1,137 @@
// 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 "RoomDataFactory.h"
#include "RoomData.h"
#include "AssetTypeCategories.h"
#include "ProceduralDungeonEditor.h"
#include "ClassViewerModule.h"
#include "ClassViewerFilter.h"
#include "Kismet2/SClassPickerDialog.h"
#include "ProceduralDungeonEditorSettings.h"
#include "Misc/EngineVersionComparison.h"
#define LOCTEXT_NAMESPACE "RoomDataFactory"
// Copied and a little modified from the EditorFactories.cpp of UnrealEd module (not public)
class FRoomDataClassFilter : public IClassViewerFilter
{
public:
FRoomDataClassFilter()
: DisallowedClassFlags(CLASS_Abstract | CLASS_Deprecated | CLASS_NewerVersionExists | CLASS_HideDropDown)
{
}
/** Disallowed class flags. */
EClassFlags DisallowedClassFlags;
virtual bool IsClassAllowed(const FClassViewerInitializationOptions& InInitOptions, const UClass* InClass, TSharedRef<FClassViewerFilterFuncs> InFilterFuncs) override
{
bool bAllowed = !InClass->HasAnyClassFlags(DisallowedClassFlags)
&& InFilterFuncs->IfInChildOfClassesSet(ParentClasses, InClass) != EFilterReturn::Failed;
return bAllowed;
}
virtual bool IsUnloadedClassAllowed(const FClassViewerInitializationOptions& InInitOptions, const TSharedRef<const IUnloadedBlueprintData> InUnloadedClassData, TSharedRef<FClassViewerFilterFuncs> InFilterFuncs) override
{
return !InUnloadedClassData->HasAnyClassFlags(DisallowedClassFlags)
&& InFilterFuncs->IfInChildOfClassesSet(ParentClasses, InUnloadedClassData) != EFilterReturn::Failed;
}
private:
/** All children of these classes will be included unless filtered out by another setting. */
static TSet<const UClass*> ParentClasses;
};
TSet<const UClass*> FRoomDataClassFilter::ParentClasses {URoomData::StaticClass()};
URoomDataFactory::URoomDataFactory()
{
bCreateNew = true;
bEditAfterNew = true;
SupportedClass = URoomData::StaticClass();
}
uint32 URoomDataFactory::GetMenuCategories() const
{
FProceduralDungeonEditorModule& EditorModule = FModuleManager::LoadModuleChecked<FProceduralDungeonEditorModule>("ProceduralDungeonEditor");
return EditorModule.GetAssetTypeCategory();
}
bool URoomDataFactory::ConfigureProperties()
{
// Get the default room data class from the editor settings
const UProceduralDungeonEditorSettings* EditorSettings = GetDefault<UProceduralDungeonEditorSettings>();
UClass* DefaultClass = EditorSettings->DefaultRoomDataClass.Get();
if (!DefaultClass)
DefaultClass = URoomData::StaticClass();
// TODO: want to show only RootClass and children with a plugin setting
// However the SClassPickerDialog will always show all parent classes of the RootClass
// (see SClassPickerDialog.cpp line 49)
UClass* RootClass = URoomData::StaticClass();
//if (EditorSettings->bShowOnlyDefaultAndChildren)
// RootClass = DefaultClass;
// Bypass the class picker if there is no derived class of the default class
if (EditorSettings->bUseDefaultIfNoChild)
{
TArray<UClass*> DerivedClasses;
GetDerivedClasses(DefaultClass, DerivedClasses, false);
if (DerivedClasses.Num() <= 0)
{
RoomDataClass = DefaultClass;
return true;
}
}
// nullptr the RoomDataClass so we can check for selection
RoomDataClass = nullptr;
// Load the classviewer module to display a class picker
FClassViewerModule& ClassViewerModule = FModuleManager::LoadModuleChecked<FClassViewerModule>("ClassViewer");
// The class viewer filter to show only RoomData class and its child classes
TSharedPtr<FRoomDataClassFilter> Filter = MakeShareable(new FRoomDataClassFilter);
// Fill in options
FClassViewerInitializationOptions Options;
Options.Mode = EClassViewerMode::ClassPicker;
Options.InitiallySelectedClass = DefaultClass;
#if UE_VERSION_OLDER_THAN(5, 0, 0)
Options.ClassFilter = Filter;
#else
Options.ClassFilters.Add(Filter.ToSharedRef());
#endif
const FText TitleText = LOCTEXT("CreateRoomDataOptions", "Pick Room Data Class");
UClass* ChosenClass = nullptr;
const bool bPressedOk = SClassPickerDialog::PickClass(TitleText, Options, ChosenClass, RootClass);
if (bPressedOk)
{
RoomDataClass = ChosenClass;
}
return bPressedOk;
}
UObject* URoomDataFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
if (RoomDataClass != nullptr)
{
return NewObject<URoomData>(InParent, RoomDataClass, InName, Flags | RF_Transactional);
}
else
{
// if we have no data asset class, use the passed-in class instead
check(InClass->IsChildOf(URoomData::StaticClass()));
return NewObject<URoomData>(InParent, InClass, InName, Flags);
}
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,28 @@
// 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.
#pragma once
#include "CoreMinimal.h"
#include "Factories/Factory.h"
#include "RoomDataFactory.generated.h"
UCLASS()
class URoomDataFactory : public UFactory
{
GENERATED_BODY()
public:
URoomDataFactory();
virtual uint32 GetMenuCategories() const override;
virtual bool ConfigureProperties() override;
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn);
protected:
TSubclassOf<class URoomData> RoomDataClass;
};
@@ -0,0 +1,10 @@
// 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 "ProceduralDungeonEdLog.h"
DEFINE_LOG_CATEGORY(LogProceduralDungeonEditor);
@@ -0,0 +1,21 @@
// 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.
#pragma once
#include "CoreMinimal.h"
DECLARE_LOG_CATEGORY_EXTERN(LogProceduralDungeonEditor, Verbose, All);
#define DungeonEd_LogInfo(Format, ...) \
UE_LOG(LogProceduralDungeonEditor, Log, TEXT(Format), ##__VA_ARGS__)
#define DungeonEd_LogWarning(Format, ...) \
UE_LOG(LogProceduralDungeonEditor, Warning, TEXT(Format), ##__VA_ARGS__)
#define DungeonEd_LogError(Format, ...) \
UE_LOG(LogProceduralDungeonEditor, Error, TEXT(Format), ##__VA_ARGS__)
@@ -0,0 +1,27 @@
// 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 "ProceduralDungeonEdTypes.h"
EAxisList::Type operator~(const EAxisList::Type& This)
{
return static_cast<EAxisList::Type>(EAxisList::All - This);
}
EAxisList::Type& operator&=(EAxisList::Type& This, const EAxisList::Type& Other)
{
This = static_cast<EAxisList::Type>(This & Other);
return This;
}
FBoxCenterAndExtent FMargin3D::Apply(const FBoxCenterAndExtent& Bounds) const
{
FBoxCenterAndExtent NewBounds(Bounds);
NewBounds.Extent += 0.5f * FVector(XAxis.X + XAxis.Y, YAxis.X + YAxis.Y, ZAxis.X + ZAxis.Y);
NewBounds.Center += 0.5f * FVector(XAxis.X - XAxis.Y, YAxis.X - YAxis.Y, ZAxis.X - ZAxis.Y);
return NewBounds;
}
@@ -0,0 +1,59 @@
// 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.
#pragma once
#include "ProceduralDungeonTypes.h"
#include "Misc/EngineVersionComparison.h"
#include "Math/GenericOctree.h" // FBoxCenterAndExtent
#include "ProceduralDungeonEdTypes.generated.h"
#if UE_VERSION_OLDER_THAN(5, 0, 0)
#define COMPATIBILITY 1
#else
#define COMPATIBILITY 0
#endif
#if !COMPATIBILITY
#include "UnrealWidgetFwd.h"
#endif
#if COMPATIBILITY
using WidgetMode = FWidget::EWidgetMode;
#else
using WidgetMode = UE::Widget::EWidgetMode;
#endif
#if UE_VERSION_NEWER_THAN(5, 3, 0)
#define GC_PTR(VAR_TYPE) TObjectPtr<VAR_TYPE>
#else
#define GC_PTR(VAR_TYPE) VAR_TYPE*
#endif
// Some utility functions for EAxisList
EAxisList::Type operator~(const EAxisList::Type& This);
EAxisList::Type& operator&=(EAxisList::Type& This, const EAxisList::Type& Other);
// Holds margin values in 3D (e.g. used for box volumes)
USTRUCT()
struct FMargin3D
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, Category = "Margin", meta = (DisplayName = "X"))
FVector2D XAxis {0.0f, 0.0f};
UPROPERTY(EditAnywhere, Category = "Margin", meta = (DisplayName = "Y"))
FVector2D YAxis {0.0f, 0.0f};
UPROPERTY(EditAnywhere, Category = "Margin", meta = (DisplayName = "Z"))
FVector2D ZAxis {0.0f, 0.0f};
// Create a new bounds from an existing bounds with the margin applied on it.
FBoxCenterAndExtent Apply(const FBoxCenterAndExtent& Bounds) const;
};
@@ -0,0 +1,141 @@
// 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 "ProceduralDungeonEditor.h"
#include "ProceduralDungeonEdLog.h"
#include "Customizations/DoorDefCustomization.h"
#include "Customizations/Margin3DCustomization.h"
#include "ProceduralDungeonTypes.h"
#include "IAssetTools.h"
#include "Interfaces/IPluginManager.h"
#include "AssetToolsModule.h"
#include "AssetTypeActions/AssetTypeActions_RoomData.h"
#include "AssetTypeActions/AssetTypeActions_DoorType.h"
#include "Developer/Settings/Public/ISettingsModule.h"
#include "Developer/Settings/Public/ISettingsSection.h"
#include "Styling/SlateStyle.h"
#include "Styling/SlateStyleRegistry.h"
#include "ProceduralDungeonEditorSettings.h"
#include "EditorMode/ProceduralDungeonEdMode.h"
#include "EditorMode/ProceduralDungeonEditorCommands.h"
#define LOCTEXT_NAMESPACE "FProceduralDungeonEditorModule"
#define IMAGE_BRUSH(RelativePath, ...) FSlateImageBrush(StyleSet->RootToContentDir(RelativePath, TEXT(".png")), __VA_ARGS__)
void FProceduralDungeonEditorModule::StartupModule()
{
RegisterSettings();
FProceduralDungeonEditorCommands::Register();
// Register assets in the "Procedural Dungeon" category
{
IAssetTools& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
AssetTypeCategory = AssetToolsModule.RegisterAdvancedAssetCategory(FName(TEXT("ProceduralDungeon")), LOCTEXT("ProceduralDungeonCategory", "Procedural Dungeon"));
TSharedPtr<FAssetTypeActions_RoomData> RoomDataAction = MakeShareable(new FAssetTypeActions_RoomData());
AssetToolsModule.RegisterAssetTypeActions(RoomDataAction.ToSharedRef());
TSharedPtr<FAssetTypeActions_DoorType> DoorTypeAction = MakeShareable(new FAssetTypeActions_DoorType());
AssetToolsModule.RegisterAssetTypeActions(DoorTypeAction.ToSharedRef());
}
// Register detail customizations
{
FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyModule.RegisterCustomPropertyTypeLayout(FDoorDef::StaticStruct()->GetFName(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FDoorDefCustomization::MakeInstance));
PropertyModule.RegisterCustomPropertyTypeLayout(FMargin3D::StaticStruct()->GetFName(), FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FMargin3DCustomization::MakeInstance));
PropertyModule.NotifyCustomizationModuleChanged();
}
// Register slate style set
{
StyleSet = MakeShareable(new FSlateStyleSet("ProceduralDungeonStyle"));
FString ContentDir = IPluginManager::Get().FindPlugin("ProceduralDungeon")->GetBaseDir();
StyleSet->SetContentRoot(ContentDir);
const FVector2D Icon20x20(20.0f, 20.0f);
const FVector2D Icon40x40(40.0f, 40.0f);
StyleSet->Set("ProceduralDungeon.Icon", new IMAGE_BRUSH("Resources/IconEditorMode", Icon40x40));
StyleSet->Set("ProceduralDungeon.Icon.Small", new IMAGE_BRUSH("Resources/IconEditorMode", Icon20x20));
StyleSet->Set("ProceduralDungeonEditor.SizeTool", new IMAGE_BRUSH("Resources/IconSizeTool", Icon20x20));
StyleSet->Set("ProceduralDungeonEditor.SizeTool.Small", new IMAGE_BRUSH("Resources/IconSizeTool", Icon20x20));
StyleSet->Set("ProceduralDungeonEditor.DoorTool", new IMAGE_BRUSH("Resources/IconDoorTool", Icon20x20));
StyleSet->Set("ProceduralDungeonEditor.DoorTool.Small", new IMAGE_BRUSH("Resources/IconDoorTool", Icon20x20));
FSlateStyleRegistry::RegisterSlateStyle(*StyleSet);
}
FSlateIcon ModeIcon = FSlateIcon("ProceduralDungeonStyle", "ProceduralDungeon.Icon", "ProceduralDungeon.Icon.Small");
FEditorModeRegistry::Get().RegisterMode<FProceduralDungeonEdMode>(FProceduralDungeonEdMode::EM_ProceduralDungeon, LOCTEXT("ProceduralDungeonEdModeName", "Dungeon Room"), ModeIcon, true);
}
void FProceduralDungeonEditorModule::ShutdownModule()
{
FProceduralDungeonEditorCommands::Unregister();
// Unregister detail customizations
if (FModuleManager::Get().IsModuleLoaded("PropertyEditor"))
{
FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyModule.UnregisterCustomPropertyTypeLayout(FDoorDef::StaticStruct()->GetFName());
PropertyModule.UnregisterCustomPropertyTypeLayout(FMargin3D::StaticStruct()->GetFName());
}
// Unregister editor mode
FEditorModeRegistry::Get().UnregisterMode(FProceduralDungeonEdMode::EM_ProceduralDungeon);
if (UObjectInitialized())
{
UnregisterSettings();
}
}
void FProceduralDungeonEditorModule::RegisterSettings()
{
if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings"))
{
// Register the settings
ISettingsSectionPtr SettingsSection = SettingsModule->RegisterSettings("Project", "Editor", "Procedural Dungeon",
LOCTEXT("ProceduralDungeonEditorSettingsName", "Procedural Dungeon"),
LOCTEXT("ProceduralDungeonEditorSettingsDescription", "Configuration for the Procedural Dungeon plugin"),
GetMutableDefault<UProceduralDungeonEditorSettings>()
);
if (SettingsSection.IsValid())
{
SettingsSection->OnModified().BindRaw(this, &FProceduralDungeonEditorModule::HandleSettingsSaved);
}
}
}
void FProceduralDungeonEditorModule::UnregisterSettings()
{
if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings"))
{
SettingsModule->UnregisterSettings("Project", "Plugins", "Procedural Dungeon");
}
}
bool FProceduralDungeonEditorModule::HandleSettingsSaved()
{
UProceduralDungeonEditorSettings* Settings = GetMutableDefault<UProceduralDungeonEditorSettings>();
bool ResaveSettings = false;
// Here check and resave if any changes have been made
if (ResaveSettings)
{
Settings->SaveConfig();
}
return true;
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FProceduralDungeonEditorModule, ProceduralDungeonEditor)
@@ -0,0 +1,23 @@
// 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 "ProceduralDungeonEditorSettings.h"
UProceduralDungeonEditorSettings::UProceduralDungeonEditorSettings(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
DefaultRoomDataClass = URoomData::StaticClass();
bUseDefaultIfNoChild = true;
//bShowOnlyDefaultAndChildren = false;
DefaultMargins.XAxis.X = 10.0f;
DefaultMargins.XAxis.Y = 10.0f;
DefaultMargins.YAxis.X = 10.0f;
DefaultMargins.YAxis.Y = 10.0f;
DefaultMargins.ZAxis.X = 10.0f;
DefaultMargins.ZAxis.Y = 10.0f;
}
@@ -0,0 +1,45 @@
// 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.
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "RoomData.h"
#include "ProceduralDungeonEdTypes.h"
#include "ProceduralDungeonEditorSettings.generated.h"
UCLASS(Config = Editor, DefaultConfig)
class UProceduralDungeonEditorSettings : public UObject
{
GENERATED_BODY()
public:
UProceduralDungeonEditorSettings(const FObjectInitializer& ObjectInitializer);
// The default RoomData class to use in the class picker when creating a new RoomData asset.
UPROPERTY(Config, EditAnywhere, Category = "General", NoClear, meta = (AllowAbstract = false))
TSubclassOf<URoomData> DefaultRoomDataClass;
// The class picker will not show if the default RoomData class has no child classes
UPROPERTY(Config, EditAnywhere, Category = "General", meta = (DisplayName = "Use Automatically Default Class If No Child"))
bool bUseDefaultIfNoChild;
// The class picker will show only the default RoomData and its children
//UPROPERTY(Config, EditAnywhere, Category = "General")
//bool bShowOnlyDefaultAndChildren;
// Default margin values on each axis to update volumes in Room Editor mode.
UPROPERTY(Config, EditAnywhere, Category = "Room Editor Mode")
FMargin3D DefaultMargins;
// When ticked the orbit rotation of the camera will use the room bounds center as pivot point
// instead of the current selected actor when the Room Editor Mode is active.
// (Works only when "Orbit Camera Around Selection" is ticked in your Editor Preferences)
UPROPERTY(Config, EditAnywhere, Category = "Room Editor Mode", meta = (DisplayName = "Use the room bounds center as pivot for camera orbit"))
bool bUseRoomAsOrbitPivot {true};
};
@@ -0,0 +1,40 @@
// Copyright Benoit Pelletier 2019 - 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.
using UnrealBuildTool;
public class ProceduralDungeonEditor : ModuleRules
{
public ProceduralDungeonEditor(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
// Uncomment that to detect when there are missing includes in cpp files
//bUseUnity = false;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "Projects" });
PrivateDependencyModuleNames.AddRange(new string[] {
"ProceduralDungeon",
"CoreUObject",
"InputCore",
"PropertyEditor",
"DetailCustomizations",
"UnrealEd",
"AssetTools",
"ClassViewer",
"SlateCore",
"Slate",
"EditorStyle",
#if UE_5_0_OR_LATER
"EditorFramework",
#endif
"Engine"
}
);
}
}
@@ -0,0 +1,34 @@
// 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.
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
#include "AssetTypeCategories.h"
class FSlateStyleSet;
class FProceduralDungeonEditorModule : public IModuleInterface
{
public:
// ~BEGIN IModuleInterface
virtual void StartupModule() override;
virtual void ShutdownModule() override;
// ~END IModuleInterface
FORCEINLINE EAssetTypeCategories::Type GetAssetTypeCategory() const { return AssetTypeCategory; }
private:
void RegisterSettings();
void UnregisterSettings();
bool HandleSettingsSaved();
private:
EAssetTypeCategories::Type AssetTypeCategory {EAssetTypeCategories::Type::None};
TSharedPtr<FSlateStyleSet> StyleSet;
};