Update directive utilities plugin

This commit is contained in:
2026-08-15 21:06:43 +02:00
parent 2fe7f83a38
commit a06fed1cba
103 changed files with 18211 additions and 1071 deletions

View File

@@ -16,6 +16,7 @@ public class DirectiveUtilitiesEditor : ModuleRules
"Engine",
"EditorSubsystem",
"EditorScriptingUtilities",
"AssetRegistry",
"DirectiveUtilitiesRuntime",
}
);
@@ -26,9 +27,8 @@ public class DirectiveUtilitiesEditor : ModuleRules
"Slate",
"SlateCore",
"UnrealEd",
"AssetRegistry",
"AssetTools",
}
);
}
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "AssetRegistry/IAssetRegistry.h"
namespace DirectiveUtilitiesEditor
{
inline void EnsureAssetRegistryScan(IAssetRegistry& AssetRegistry)
{
if (!AssetRegistry.IsSearchAllAssets())
{
AssetRegistry.SearchAllAssets(true);
}
if (AssetRegistry.IsLoadingAssets())
{
AssetRegistry.WaitForCompletion();
}
}
}

View File

@@ -0,0 +1,123 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "AssetRegistry/DirectiveUtilDependencyCycleFinder.h"
namespace DirectiveUtilitiesEditor
{
FDependencyCycleFinder::FDependencyCycleFinder(const TMap<FName, TArray<FName>>& InGraph)
: Graph(InGraph)
{
}
TArray<FDirectiveUtilAssetDependencyCycle> FDependencyCycleFinder::Find() const
{
const TArray<FName> FinishOrder = BuildFinishOrder();
const TMap<FName, TArray<FName>> ReverseGraph = BuildReverseGraph();
TSet<FName> Visited;
TArray<FDirectiveUtilAssetDependencyCycle> Cycles;
for (int32 Index = FinishOrder.Num() - 1; Index >= 0; --Index)
{
const FName Root = FinishOrder[Index];
if (Visited.Contains(Root))
{
continue;
}
FDirectiveUtilAssetDependencyCycle Cycle;
TArray<FName> Pending = {Root};
Visited.Add(Root);
while (!Pending.IsEmpty())
{
const FName Package = Pending.Pop(EAllowShrinking::No);
Cycle.Packages.Add(Package);
const TArray<FName>* Referencers = ReverseGraph.Find(Package);
if (!Referencers)
{
continue;
}
for (const FName Referencer : *Referencers)
{
if (!Visited.Contains(Referencer))
{
Visited.Add(Referencer);
Pending.Add(Referencer);
}
}
}
const TArray<FName>* RootDependencies = Cycle.Packages.Num() == 1
? Graph.Find(Cycle.Packages[0])
: nullptr;
const bool bSelfCycle = RootDependencies && RootDependencies->Contains(Cycle.Packages[0]);
if (Cycle.Packages.Num() > 1 || bSelfCycle)
{
Cycle.Packages.Sort(FNameLexicalLess());
Cycles.Add(MoveTemp(Cycle));
}
}
Cycles.Sort([](const FDirectiveUtilAssetDependencyCycle& Left, const FDirectiveUtilAssetDependencyCycle& Right) {
return Left.Packages[0].LexicalLess(Right.Packages[0]);
});
return Cycles;
}
TArray<FName> FDependencyCycleFinder::BuildFinishOrder() const
{
TArray<FName> Packages;
Graph.GetKeys(Packages);
Packages.Sort(FNameLexicalLess());
TArray<FName> FinishOrder;
FinishOrder.Reserve(Packages.Num());
TSet<FName> Visited;
for (const FName Root : Packages)
{
if (Visited.Contains(Root))
{
continue;
}
TArray<FTraversalFrame> Pending = {{Root, 0}};
Visited.Add(Root);
while (!Pending.IsEmpty())
{
FTraversalFrame& Frame = Pending.Last();
const TArray<FName>* Dependencies = Graph.Find(Frame.Package);
if (Dependencies && Frame.NextDependencyIndex < Dependencies->Num())
{
const FName Dependency = (*Dependencies)[Frame.NextDependencyIndex++];
if (!Visited.Contains(Dependency))
{
Visited.Add(Dependency);
Pending.Add({Dependency, 0});
}
continue;
}
FinishOrder.Add(Frame.Package);
Pending.Pop(EAllowShrinking::No);
}
}
return FinishOrder;
}
TMap<FName, TArray<FName>> FDependencyCycleFinder::BuildReverseGraph() const
{
TMap<FName, TArray<FName>> ReverseGraph;
for (const TPair<FName, TArray<FName>>& Node : Graph)
{
ReverseGraph.FindOrAdd(Node.Key);
for (const FName Dependency : Node.Value)
{
ReverseGraph.FindOrAdd(Dependency).Add(Node.Key);
}
}
for (TPair<FName, TArray<FName>>& Node : ReverseGraph)
{
Node.Value.Sort(FNameLexicalLess());
}
return ReverseGraph;
}
}

View File

@@ -0,0 +1,29 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Types/DirectiveUtilEditorAuditTypes.h"
namespace DirectiveUtilitiesEditor
{
class DIRECTIVEUTILITIESEDITOR_API FDependencyCycleFinder
{
public:
explicit FDependencyCycleFinder(const TMap<FName, TArray<FName>>& InGraph);
TArray<FDirectiveUtilAssetDependencyCycle> Find() const;
private:
struct FTraversalFrame
{
FName Package;
int32 NextDependencyIndex = 0;
};
TArray<FName> BuildFinishOrder() const;
TMap<FName, TArray<FName>> BuildReverseGraph() const;
const TMap<FName, TArray<FName>>& Graph;
};
}

View File

@@ -1,6 +1,7 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "DirectiveUtilitiesEditor.h"
#include "Tasks/DirectiveUtilEditorSlowTask.h"
void FDirectiveUtilitiesEditorModule::StartupModule()
{
@@ -8,6 +9,7 @@ void FDirectiveUtilitiesEditorModule::StartupModule()
void FDirectiveUtilitiesEditorModule::ShutdownModule()
{
UDirectiveUtilEditorSlowTask::FinishActiveTask();
}
IMPLEMENT_MODULE(FDirectiveUtilitiesEditorModule, DirectiveUtilitiesEditor)

View File

@@ -0,0 +1,433 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilEditorAssetAuditLibrary.h"
#include "AssetRegistry/DirectiveUtilAssetRegistry.h"
#include "AssetRegistry/DirectiveUtilDependencyCycleFinder.h"
#include "AssetRegistry/ARFilter.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "Engine/AssetManager.h"
#include "HAL/FileManager.h"
#include "Misc/PackageName.h"
#include "UObject/ObjectRedirector.h"
namespace
{
using namespace UE::AssetRegistry;
bool IsPackageInsideAuditPath(const FName PackageName, const FName Path)
{
const FString Package = PackageName.ToString();
FString Parent = Path.ToString();
Parent.RemoveFromEnd(TEXT("/"));
return Package == Parent || Package.StartsWith(Parent + TEXT("/"));
}
bool ContainsPathSegment(const FName PackagePath, const FString& Segment)
{
const FString Path = TEXT("/") + PackagePath.ToString().TrimChar(TEXT('/')) + TEXT("/");
return Path.Contains(TEXT("/") + Segment + TEXT("/"));
}
bool IsExcluded(const FAssetData& Asset, const FDirectiveUtilAssetAuditOptions& Options)
{
if (Asset.AssetClassPath == UObjectRedirector::StaticClass()->GetClassPathName())
{
return true;
}
if (Options.bUseDefaultPathExclusions
&& (ContainsPathSegment(Asset.PackagePath, TEXT("Developers"))
|| ContainsPathSegment(Asset.PackagePath, TEXT("__ExternalActors__"))
|| ContainsPathSegment(Asset.PackagePath, TEXT("__ExternalObjects__"))
|| ContainsPathSegment(Asset.PackagePath, TEXT("Test"))
|| ContainsPathSegment(Asset.PackagePath, TEXT("Tests"))))
{
return true;
}
for (const FName Path : Options.ExcludedPackagePaths)
{
if (IsPackageInsideAuditPath(Asset.PackageName, Path))
{
return true;
}
}
return false;
}
TArray<FAssetData> GetScannedAssets(IAssetRegistry& AssetRegistry, const FDirectiveUtilAssetAuditOptions& Options)
{
DirectiveUtilitiesEditor::EnsureAssetRegistryScan(AssetRegistry);
FARFilter Filter;
Filter.bRecursivePaths = true;
if (Options.PackagePaths.IsEmpty())
{
Filter.PackagePaths.Add(TEXT("/Game"));
}
else
{
Filter.PackagePaths.Append(Options.PackagePaths);
}
TArray<FAssetData> Assets;
AssetRegistry.GetAssets(Filter, Assets);
Assets.RemoveAll([&Options](const FAssetData& Asset) {
return !Asset.IsValid() || IsExcluded(Asset, Options);
});
Assets.Sort([](const FAssetData& Left, const FAssetData& Right) {
return Left.GetSoftObjectPath().LexicalLess(Right.GetSoftObjectPath());
});
return Assets;
}
bool IsPrimaryAsset(const FAssetData& Asset)
{
const UAssetManager* AssetManager = UAssetManager::GetIfInitialized();
return AssetManager && AssetManager->GetPrimaryAssetIdForData(Asset).IsValid();
}
EDirectiveUtilAssetDependencyType GetDependencyType(const FAssetDependency& Dependency)
{
if (EnumHasAnyFlags(Dependency.Category, EDependencyCategory::SearchableName))
{
return EDirectiveUtilAssetDependencyType::SearchableName;
}
if (EnumHasAnyFlags(Dependency.Category, EDependencyCategory::Manage))
{
return EnumHasAnyFlags(Dependency.Properties, EDependencyProperty::Direct)
? EDirectiveUtilAssetDependencyType::DirectManagement
: EDirectiveUtilAssetDependencyType::IndirectManagement;
}
return EnumHasAnyFlags(Dependency.Properties, EDependencyProperty::Hard)
? EDirectiveUtilAssetDependencyType::HardPackage
: EDirectiveUtilAssetDependencyType::SoftPackage;
}
bool PackageExists(IAssetRegistry& AssetRegistry, const FName PackageName)
{
if (PackageName.IsNone())
{
return true;
}
const FString Package = PackageName.ToString();
if (Package.StartsWith(TEXT("/Script/")) || Package.StartsWith(TEXT("/Memory/")) || Package.StartsWith(TEXT("/Temp/")))
{
return true;
}
if (FindPackage(nullptr, *Package))
{
return true;
}
TArray<FAssetData> PackageAssets;
return AssetRegistry.GetAssetsByPackageName(PackageName, PackageAssets, true) && !PackageAssets.IsEmpty();
}
class FAssetAuditScan
{
public:
FAssetAuditScan(IAssetRegistry& InAssetRegistry, const FDirectiveUtilAssetAuditOptions& InOptions)
: AssetRegistry(InAssetRegistry)
, bIncludePrimaryAssets(InOptions.bIncludePrimaryAssets)
, Assets(GetScannedAssets(InAssetRegistry, InOptions))
{
}
const TArray<FAssetData>& GetAssets() const
{
return Assets;
}
const TArray<FAssetDependency>& GetDependencies(const FName PackageName)
{
if (const TArray<FAssetDependency>* Existing = Dependencies.Find(PackageName))
{
return *Existing;
}
TArray<FAssetDependency>& Result = Dependencies.Add(PackageName);
AssetRegistry.GetDependencies(FAssetIdentifier(PackageName), Result, EDependencyCategory::All);
return Result;
}
const TArray<FName>& GetReferencers(const FName PackageName)
{
if (const TArray<FName>* Existing = Referencers.Find(PackageName))
{
return *Existing;
}
TArray<FName>& Result = Referencers.Add(PackageName);
AssetRegistry.GetReferencers(PackageName, Result, EDependencyCategory::All);
Result.Remove(PackageName);
Result.Sort(FNameLexicalLess());
return Result;
}
bool IncludesPrimaryAssets() const
{
return bIncludePrimaryAssets;
}
IAssetRegistry& GetAssetRegistry() const
{
return AssetRegistry;
}
private:
IAssetRegistry& AssetRegistry;
bool bIncludePrimaryAssets;
TArray<FAssetData> Assets;
TMap<FName, TArray<FAssetDependency>> Dependencies;
TMap<FName, TArray<FName>> Referencers;
};
FString EscapeCsv(const FString& Value)
{
FString Escaped = Value.Replace(TEXT("\""), TEXT("\"\""));
return FString::Printf(TEXT("\"%s\""), *Escaped);
}
FString DependencyTypeToString(const EDirectiveUtilAssetDependencyType Type)
{
switch (Type)
{
case EDirectiveUtilAssetDependencyType::HardPackage:
return TEXT("Hard Package");
case EDirectiveUtilAssetDependencyType::SoftPackage:
return TEXT("Soft Package");
case EDirectiveUtilAssetDependencyType::SearchableName:
return TEXT("Searchable Name");
case EDirectiveUtilAssetDependencyType::DirectManagement:
return TEXT("Direct Management");
case EDirectiveUtilAssetDependencyType::IndirectManagement:
return TEXT("Indirect Management");
}
return TEXT("Unknown");
}
TArray<FAssetData> FindUnreferencedCandidates(FAssetAuditScan& Scan)
{
TArray<FAssetData> Candidates;
for (const FAssetData& Asset : Scan.GetAssets())
{
if ((!IsPrimaryAsset(Asset) || Scan.IncludesPrimaryAssets()) && Scan.GetReferencers(Asset.PackageName).IsEmpty())
{
Candidates.Add(Asset);
}
}
return Candidates;
}
TArray<FDirectiveUtilMissingAssetReference> FindMissingReferences(FAssetAuditScan& Scan)
{
TArray<FDirectiveUtilMissingAssetReference> MissingReferences;
for (const FAssetData& Asset : Scan.GetAssets())
{
for (const FAssetDependency& Dependency : Scan.GetDependencies(Asset.PackageName))
{
if (!PackageExists(Scan.GetAssetRegistry(), Dependency.AssetId.PackageName))
{
FDirectiveUtilMissingAssetReference& MissingReference = MissingReferences.AddDefaulted_GetRef();
MissingReference.ReferencingAsset = Asset;
MissingReference.MissingPackage = Dependency.AssetId.PackageName;
MissingReference.DependencyType = GetDependencyType(Dependency);
}
}
}
MissingReferences.Sort([](const FDirectiveUtilMissingAssetReference& Left, const FDirectiveUtilMissingAssetReference& Right) {
if (Left.ReferencingAsset.PackageName != Right.ReferencingAsset.PackageName)
{
return Left.ReferencingAsset.PackageName.LexicalLess(Right.ReferencingAsset.PackageName);
}
return Left.MissingPackage.LexicalLess(Right.MissingPackage);
});
return MissingReferences;
}
TArray<FDirectiveUtilAssetDependencyCycle> FindDependencyCycles(FAssetAuditScan& Scan)
{
TSet<FName> ScannedPackages;
for (const FAssetData& Asset : Scan.GetAssets())
{
ScannedPackages.Add(Asset.PackageName);
}
TMap<FName, TArray<FName>> Graph;
for (const FName Package : ScannedPackages)
{
TArray<FName>& Edges = Graph.Add(Package);
for (const FAssetDependency& Dependency : Scan.GetDependencies(Package))
{
if (ScannedPackages.Contains(Dependency.AssetId.PackageName))
{
Edges.AddUnique(Dependency.AssetId.PackageName);
}
}
Edges.Sort(FNameLexicalLess());
}
return DirectiveUtilitiesEditor::FDependencyCycleFinder(Graph).Find();
}
}
TArray<FAssetData> UDirectiveUtilEditorAssetAuditLibrary::FindUnreferencedAssetCandidates(
const FDirectiveUtilAssetAuditOptions& Options)
{
IAssetRegistry* AssetRegistry = IAssetRegistry::Get();
if (!AssetRegistry)
{
return {};
}
FAssetAuditScan Scan(*AssetRegistry, Options);
return FindUnreferencedCandidates(Scan);
}
TArray<FDirectiveUtilMissingAssetReference> UDirectiveUtilEditorAssetAuditLibrary::FindMissingAssetReferences(
const FDirectiveUtilAssetAuditOptions& Options)
{
IAssetRegistry* AssetRegistry = IAssetRegistry::Get();
if (!AssetRegistry)
{
return {};
}
FAssetAuditScan Scan(*AssetRegistry, Options);
return FindMissingReferences(Scan);
}
TArray<FDirectiveUtilAssetDependencyCycle> UDirectiveUtilEditorAssetAuditLibrary::FindAssetDependencyCycles(
const FDirectiveUtilAssetAuditOptions& Options)
{
IAssetRegistry* AssetRegistry = IAssetRegistry::Get();
if (!AssetRegistry)
{
return {};
}
FAssetAuditScan Scan(*AssetRegistry, Options);
return FindDependencyCycles(Scan);
}
FDirectiveUtilAssetAuditReport UDirectiveUtilEditorAssetAuditLibrary::BuildAssetAuditReport(
const FDirectiveUtilAssetAuditOptions& Options)
{
FDirectiveUtilAssetAuditReport Report;
IAssetRegistry* AssetRegistry = IAssetRegistry::Get();
if (!AssetRegistry)
{
return Report;
}
FAssetAuditScan Scan(*AssetRegistry, Options);
const TArray<FAssetData> Candidates = FindUnreferencedCandidates(Scan);
Report.MissingReferences = FindMissingReferences(Scan);
Report.DependencyCycles = FindDependencyCycles(Scan);
TSet<FSoftObjectPath> CandidatePaths;
for (const FAssetData& Candidate : Candidates)
{
CandidatePaths.Add(Candidate.GetSoftObjectPath());
}
TMap<FName, int32> MissingCounts;
for (const FDirectiveUtilMissingAssetReference& MissingReference : Report.MissingReferences)
{
++MissingCounts.FindOrAdd(MissingReference.ReferencingAsset.PackageName);
}
TSet<FName> CyclicPackages;
for (const FDirectiveUtilAssetDependencyCycle& Cycle : Report.DependencyCycles)
{
CyclicPackages.Append(Cycle.Packages);
}
for (const FAssetData& Asset : Scan.GetAssets())
{
FDirectiveUtilAssetAuditEntry& Entry = Report.Assets.AddDefaulted_GetRef();
Entry.Asset = Asset;
Entry.PackageName = Asset.PackageName;
Entry.PackagePath = Asset.PackagePath;
Entry.AssetClass = Asset.AssetClassPath.ToString();
Entry.bPrimaryAsset = IsPrimaryAsset(Asset);
Entry.DependencyCount = Scan.GetDependencies(Asset.PackageName).Num();
Entry.ReferencerCount = Scan.GetReferencers(Asset.PackageName).Num();
FString PackageFilename;
if (FPackageName::DoesPackageExist(Asset.PackageName.ToString(), &PackageFilename))
{
Entry.DiskSize = FMath::Max<int64>(IFileManager::Get().FileSize(*PackageFilename), 0);
}
if (CandidatePaths.Contains(Asset.GetSoftObjectPath()))
{
Entry.Findings.Add(TEXT("Unreferenced candidate"));
}
if (const int32* MissingCount = MissingCounts.Find(Asset.PackageName))
{
Entry.Findings.Add(FString::Printf(TEXT("%d missing reference%s"), *MissingCount, *MissingCount == 1 ? TEXT("") : TEXT("s")));
}
if (CyclicPackages.Contains(Asset.PackageName))
{
Entry.Findings.Add(TEXT("Dependency cycle"));
}
}
return Report;
}
FString UDirectiveUtilEditorAssetAuditLibrary::AssetAuditReportToCsv(const FDirectiveUtilAssetAuditReport& Report)
{
TArray<FString> Rows;
Rows.Add(TEXT("Asset,Package,Path,Class,Disk Size,Dependencies,Referencers,Primary Asset,Findings"));
for (const FDirectiveUtilAssetAuditEntry& Entry : Report.Assets)
{
const TArray<FString> Columns = {
EscapeCsv(Entry.Asset.GetSoftObjectPath().ToString()),
EscapeCsv(Entry.PackageName.ToString()),
EscapeCsv(Entry.PackagePath.ToString()),
EscapeCsv(Entry.AssetClass),
LexToString(Entry.DiskSize),
LexToString(Entry.DependencyCount),
LexToString(Entry.ReferencerCount),
Entry.bPrimaryAsset ? TEXT("true") : TEXT("false"),
EscapeCsv(FString::Join(Entry.Findings, TEXT("; "))),
};
Rows.Add(FString::Join(Columns, TEXT(",")));
}
Rows.Add(TEXT(""));
Rows.Add(TEXT("Missing Reference,Referencing Asset,Dependency Type"));
for (const FDirectiveUtilMissingAssetReference& MissingReference : Report.MissingReferences)
{
const TArray<FString> Columns = {
EscapeCsv(MissingReference.MissingPackage.ToString()),
EscapeCsv(MissingReference.ReferencingAsset.GetSoftObjectPath().ToString()),
EscapeCsv(DependencyTypeToString(MissingReference.DependencyType)),
};
Rows.Add(FString::Join(Columns, TEXT(",")));
}
Rows.Add(TEXT(""));
Rows.Add(TEXT("Dependency Cycle"));
for (const FDirectiveUtilAssetDependencyCycle& Cycle : Report.DependencyCycles)
{
TArray<FString> Packages;
for (const FName Package : Cycle.Packages)
{
Packages.Add(Package.ToString());
}
Rows.Add(EscapeCsv(FString::Join(Packages, TEXT(" -> "))));
}
return FString::Join(Rows, TEXT("\n"));
}

View File

@@ -2,14 +2,19 @@
#include "Libraries/DirectiveUtilEditorAssetLibrary.h"
#include "AssetRegistry/DirectiveUtilAssetRegistry.h"
#include "Editor.h"
#include "Subsystems/EditorAssetSubsystem.h"
#include "Algo/Transform.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "AssetRegistry/ARFilter.h"
#include "AssetToolsModule.h"
#include "Editor.h"
#include "IAssetTools.h"
#include "Misc/App.h"
#include "Misc/AssetRegistryInterface.h"
#include "Modules/ModuleManager.h"
#include "Runtime/Launch/Resources/Version.h"
#include "UObject/ObjectRedirector.h"
namespace
@@ -18,10 +23,7 @@ namespace
{
if (IAssetRegistry* AssetRegistry = IAssetRegistry::Get())
{
if (AssetRegistry->IsLoadingAssets())
{
AssetRegistry->WaitForCompletion();
}
DirectiveUtilitiesEditor::EnsureAssetRegistryScan(*AssetRegistry);
}
}
}
@@ -116,10 +118,7 @@ EDirectiveUtilSuccessStatus UDirectiveUtilEditorAssetLibrary::FixUpRedirectorsIn
return EDirectiveUtilSuccessStatus::Failure;
}
if (AssetRegistry->IsLoadingAssets())
{
AssetRegistry->WaitForCompletion();
}
DirectiveUtilitiesEditor::EnsureAssetRegistryScan(*AssetRegistry);
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
IAssetTools& AssetTools = AssetToolsModule.Get();
@@ -160,6 +159,11 @@ EDirectiveUtilSuccessStatus UDirectiveUtilEditorAssetLibrary::FixUpRedirectorsIn
{
return EDirectiveUtilSuccessStatus::Success;
}
if (FApp::IsUnattended())
{
UE_LOG(LogTemp, Warning, TEXT("Redirector fix-up requires an interactive editor session."));
return EDirectiveUtilSuccessStatus::Failure;
}
AssetTools.FixupReferencers(Redirectors, false, ERedirectFixupMode::DeleteFixedUpRedirectors);
OutRedirectorsProcessed = Redirectors.Num();

View File

@@ -0,0 +1,268 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilEditorBlueprintLibrary.h"
#include "AssetRegistry/DirectiveUtilAssetRegistry.h"
#include "AssetRegistry/ARFilter.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "Blueprint/BlueprintSupport.h"
#include "Components/ActorComponent.h"
#include "Engine/Blueprint.h"
#include "Engine/BlueprintGeneratedClass.h"
#include "Engine/SCS_Node.h"
#include "Engine/SimpleConstructionScript.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Misc/PackageName.h"
#include "UObject/Package.h"
namespace
{
bool IsPackageInsideBlueprintSearchPath(const FName PackageName, const FName Path)
{
const FString Package = PackageName.ToString();
FString Parent = Path.ToString();
Parent.RemoveFromEnd(TEXT("/"));
return Package == Parent || Package.StartsWith(Parent + TEXT("/"));
}
TArray<FAssetData> GetBlueprintAssets(const FDirectiveUtilBlueprintSearchOptions& Options)
{
IAssetRegistry* AssetRegistry = IAssetRegistry::Get();
if (!AssetRegistry)
{
return {};
}
DirectiveUtilitiesEditor::EnsureAssetRegistryScan(*AssetRegistry);
FARFilter Filter;
Filter.ClassPaths.Add(UBlueprint::StaticClass()->GetClassPathName());
Filter.bRecursiveClasses = true;
Filter.bRecursivePaths = true;
if (Options.PackagePaths.IsEmpty())
{
Filter.PackagePaths.Add(TEXT("/Game"));
}
else
{
Filter.PackagePaths.Append(Options.PackagePaths);
}
TArray<FAssetData> Assets;
AssetRegistry->GetAssets(Filter, Assets);
Assets.RemoveAll([&Options](const FAssetData& Asset) {
for (const FName Path : Options.ExcludedPackagePaths)
{
if (IsPackageInsideBlueprintSearchPath(Asset.PackageName, Path))
{
return true;
}
}
return false;
});
Assets.Sort([](const FAssetData& Left, const FAssetData& Right) {
return Left.GetSoftObjectPath().LexicalLess(Right.GetSoftObjectPath());
});
return Assets;
}
UBlueprint* LoadBlueprint(const FAssetData& Asset)
{
return Cast<UBlueprint>(Asset.GetAsset());
}
FString GetClassObjectPath(const UClass* Class)
{
return Class ? Class->GetPathName() : FString();
}
bool IsDirectParent(const FAssetData& Asset, const UClass* ParentClass)
{
const FString ParentTag = Asset.GetTagValueRef<FString>(FBlueprintTags::ParentClassPath);
return FPackageName::ExportTextPathToObjectPath(ParentTag) == GetClassObjectPath(ParentClass);
}
bool HasComponentClass(const UBlueprint* Blueprint, const UClass* ComponentClass, const bool bIncludeDerivedComponents)
{
TSet<const UBlueprint*> Visited;
for (const UBlueprint* Current = Blueprint; Current && !Visited.Contains(Current);)
{
Visited.Add(Current);
if (Current->SimpleConstructionScript)
{
for (const USCS_Node* Node : Current->SimpleConstructionScript->GetAllNodes())
{
if (!Node || !Node->ComponentClass)
{
continue;
}
const bool bMatches = bIncludeDerivedComponents
? Node->ComponentClass->IsChildOf(ComponentClass)
: Node->ComponentClass == ComponentClass;
if (bMatches)
{
return true;
}
}
}
Current = Current->ParentClass ? UBlueprint::GetBlueprintFromClass(Current->ParentClass) : nullptr;
}
return false;
}
}
EDirectiveUtilBlueprintCompileStatus UDirectiveUtilEditorBlueprintLibrary::GetBlueprintCompileStatus(
const UBlueprint* Blueprint)
{
if (!IsValid(Blueprint))
{
return EDirectiveUtilBlueprintCompileStatus::Unknown;
}
switch (Blueprint->Status)
{
case BS_Dirty:
return EDirectiveUtilBlueprintCompileStatus::Dirty;
case BS_Error:
return EDirectiveUtilBlueprintCompileStatus::Error;
case BS_UpToDate:
return EDirectiveUtilBlueprintCompileStatus::UpToDate;
case BS_BeingCreated:
return EDirectiveUtilBlueprintCompileStatus::BeingCreated;
case BS_UpToDateWithWarnings:
return EDirectiveUtilBlueprintCompileStatus::UpToDateWithWarnings;
case BS_Unknown:
default:
return EDirectiveUtilBlueprintCompileStatus::Unknown;
}
}
TArray<FAssetData> UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsByCompileStatus(
const EDirectiveUtilBlueprintCompileStatus CompileStatus,
const FDirectiveUtilBlueprintSearchOptions& Options)
{
TArray<FAssetData> Matches;
for (const FAssetData& Asset : GetBlueprintAssets(Options))
{
if (const UBlueprint* Blueprint = LoadBlueprint(Asset))
{
if (GetBlueprintCompileStatus(Blueprint) == CompileStatus)
{
Matches.Add(Asset);
}
}
}
return Matches;
}
TArray<FAssetData> UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsByParentClass(
UClass* ParentClass,
const FDirectiveUtilBlueprintSearchOptions& Options,
const bool bIncludeDescendants)
{
TArray<FAssetData> Matches;
if (!IsValid(ParentClass))
{
return Matches;
}
for (const FAssetData& Asset : GetBlueprintAssets(Options))
{
if (!bIncludeDescendants)
{
if (IsDirectParent(Asset, ParentClass))
{
Matches.Add(Asset);
}
continue;
}
const UBlueprint* Blueprint = LoadBlueprint(Asset);
if (Blueprint && Blueprint->ParentClass && Blueprint->ParentClass->IsChildOf(ParentClass))
{
Matches.Add(Asset);
}
}
return Matches;
}
TArray<FAssetData> UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsImplementingInterface(
UClass* InterfaceClass,
const FDirectiveUtilBlueprintSearchOptions& Options)
{
TArray<FAssetData> Matches;
if (!IsValid(InterfaceClass) || !InterfaceClass->HasAnyClassFlags(CLASS_Interface))
{
return Matches;
}
const FString InterfacePath = GetClassObjectPath(InterfaceClass);
for (const FAssetData& Asset : GetBlueprintAssets(Options))
{
const FString Interfaces = Asset.GetTagValueRef<FString>(FBlueprintTags::ImplementedInterfaces);
const FString ParentTag = Asset.GetTagValueRef<FString>(FBlueprintTags::ParentClassPath);
UClass* TaggedParent = FindObject<UClass>(nullptr, *FPackageName::ExportTextPathToObjectPath(ParentTag));
if (!Interfaces.Contains(InterfacePath) && TaggedParent && !TaggedParent->ImplementsInterface(InterfaceClass))
{
continue;
}
const UBlueprint* Blueprint = LoadBlueprint(Asset);
if (Blueprint && Blueprint->GeneratedClass && Blueprint->GeneratedClass->ImplementsInterface(InterfaceClass))
{
Matches.Add(Asset);
}
}
return Matches;
}
TArray<FAssetData> UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsContainingComponentClass(
UClass* ComponentClass,
const FDirectiveUtilBlueprintSearchOptions& Options,
const bool bIncludeDerivedComponents)
{
TArray<FAssetData> Matches;
if (!IsValid(ComponentClass) || !ComponentClass->IsChildOf(UActorComponent::StaticClass()))
{
return Matches;
}
for (const FAssetData& Asset : GetBlueprintAssets(Options))
{
if (const UBlueprint* Blueprint = LoadBlueprint(Asset))
{
if (HasComponentClass(Blueprint, ComponentClass, bIncludeDerivedComponents))
{
Matches.Add(Asset);
}
}
}
return Matches;
}
TArray<FName> UDirectiveUtilEditorBlueprintLibrary::GetUnusedBlueprintVariables(
UBlueprint* Blueprint,
const bool bIncludeExternallyAccessibleVariables)
{
TArray<FName> Names;
if (!IsValid(Blueprint))
{
return Names;
}
TArray<FProperty*> UsedVariables;
TArray<FProperty*> UnusedVariables;
FBlueprintEditorUtils::GetUsedAndUnusedVariables(Blueprint, UsedVariables, UnusedVariables);
Names.Reserve(UnusedVariables.Num());
for (const FProperty* Variable : UnusedVariables)
{
if (Variable && (bIncludeExternallyAccessibleVariables || FBlueprintEditorUtils::IsPropertyPrivate(Variable)))
{
Names.Add(Variable->GetFName());
}
}
Names.Sort(FNameLexicalLess());
return Names;
}

View File

@@ -0,0 +1,65 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilEditorTaskLibrary.h"
#include "Framework/Application/SlateApplication.h"
#include "Framework/Notifications/NotificationManager.h"
#include "Styling/AppStyle.h"
#include "Tasks/DirectiveUtilEditorSlowTask.h"
#include "Widgets/Notifications/SNotificationList.h"
UDirectiveUtilEditorSlowTask* UDirectiveUtilEditorTaskLibrary::StartEditorSlowTask(
const float TotalWork,
const FText& Description,
const bool bCanCancel)
{
if (!FMath::IsFinite(TotalWork) || TotalWork <= 0.0f)
{
return nullptr;
}
UDirectiveUtilEditorSlowTask* Task = NewObject<UDirectiveUtilEditorSlowTask>();
Task->Initialize(TotalWork, Description, bCanCancel, true);
return Task->IsActive() ? Task : nullptr;
}
bool UDirectiveUtilEditorTaskLibrary::ShowEditorNotification(
const FText& Message,
const EDirectiveUtilEditorNotificationState State,
const float ExpireDuration)
{
if (!FSlateApplication::IsInitialized())
{
return false;
}
FNotificationInfo NotificationInfo(Message);
NotificationInfo.bFireAndForget = true;
NotificationInfo.ExpireDuration = FMath::IsFinite(ExpireDuration)
? FMath::Max(0.0f, ExpireDuration)
: 0.0f;
if (State == EDirectiveUtilEditorNotificationState::Warning)
{
NotificationInfo.Image = FAppStyle::GetBrush("Icons.WarningWithColor");
}
const TSharedPtr<SNotificationItem> Notification = FSlateNotificationManager::Get().AddNotification(NotificationInfo);
if (!Notification.IsValid())
{
return false;
}
switch (State)
{
case EDirectiveUtilEditorNotificationState::Success:
Notification->SetCompletionState(SNotificationItem::CS_Success);
break;
case EDirectiveUtilEditorNotificationState::Failure:
Notification->SetCompletionState(SNotificationItem::CS_Fail);
break;
default:
Notification->SetCompletionState(SNotificationItem::CS_None);
break;
}
return true;
}

View File

@@ -3,17 +3,47 @@
#include "Subsystems/DirectiveUtilEditorActorSubsystem.h"
#include "DirectiveUtilLogChannels.h"
#include "Components/StaticMeshComponent.h"
#include "Editor.h"
#include "Engine/StaticMeshActor.h"
#include "Engine/StaticMesh.h"
#include "Engine/Texture.h"
#include "Engine/Texture2D.h"
#include "Materials/Material.h"
#include "Materials/MaterialInterface.h"
#include "Materials/MaterialExpressionTextureObject.h"
#include "EditorViewportClient.h"
#include "LevelEditorViewport.h"
#include "Components/BoxComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/SphereComponent.h"
#include "Materials/MaterialExpressionTextureSample.h"
#include "Runtime/Launch/Resources/Version.h"
#include "ScopedTransaction.h"
#include "Engine/World.h"
namespace
{
bool IsFiniteVector(const FVector& Value)
{
return FMath::IsFinite(Value.X) && FMath::IsFinite(Value.Y) && FMath::IsFinite(Value.Z);
}
bool IsFiniteQuat(const FQuat& Value)
{
return FMath::IsFinite(Value.X)
&& FMath::IsFinite(Value.Y)
&& FMath::IsFinite(Value.Z)
&& FMath::IsFinite(Value.W);
}
bool MatchesTextureReference(const UTexture* Texture, const TSoftObjectPtr<UTexture2D>& TextureReference)
{
return TextureReference.IsNull()
? Texture == nullptr
: Texture && FSoftObjectPath(Texture) == TextureReference.ToSoftObjectPath();
}
// Adds an actor to OutActors when whether it matches the predicate (evaluated once across all of the
// actor's components) equals whether matches are being included. Centralizes the include/exclude
// aggregation so a multi-component actor is judged per-actor rather than per-component.
@@ -39,7 +69,7 @@ namespace
for (TSourceActor* Actor : *Source)
{
if (!Actor || Seen.Contains(Actor)) { continue; }
if (!IsValid(Actor) || Seen.Contains(Actor)) { continue; }
if (ActorMatches(Actor) == bIncludeMatches)
{
Seen.Add(Actor);
@@ -47,27 +77,163 @@ namespace
}
}
}
struct FActorLayoutData
{
AActor* Actor = nullptr;
FBox Bounds = FBox(ForceInit);
FVector Location = FVector::ZeroVector;
int32 AttachmentDepth = 0;
};
float GetAxisValue(const FVector& Vector, const EDirectiveUtilActorLayoutAxis Axis)
{
switch (Axis)
{
case EDirectiveUtilActorLayoutAxis::X:
return Vector.X;
case EDirectiveUtilActorLayoutAxis::Y:
return Vector.Y;
case EDirectiveUtilActorLayoutAxis::Z:
return Vector.Z;
}
return 0.0f;
}
void SetAxisValue(FVector& Vector, const EDirectiveUtilActorLayoutAxis Axis, const float Value)
{
switch (Axis)
{
case EDirectiveUtilActorLayoutAxis::X:
Vector.X = Value;
break;
case EDirectiveUtilActorLayoutAxis::Y:
Vector.Y = Value;
break;
case EDirectiveUtilActorLayoutAxis::Z:
Vector.Z = Value;
break;
}
}
int32 GetAttachmentDepth(const AActor* Actor)
{
int32 Depth = 0;
TSet<const AActor*> Visited;
for (const AActor* Parent = Actor ? Actor->GetAttachParentActor() : nullptr;
Parent && !Visited.Contains(Parent);
Parent = Parent->GetAttachParentActor())
{
Visited.Add(Parent);
++Depth;
}
return Depth;
}
bool IsLayoutActorValid(AActor* Actor)
{
return IsValid(Actor)
&& Actor->GetWorld()
&& !Actor->HasAnyFlags(RF_Transient)
&& !Actor->IsActorBeingDestroyed();
}
TArray<FActorLayoutData> GetLayoutActors(
const TArray<AActor*>& Actors,
FDirectiveUtilActorOperationResult& Result)
{
TArray<FActorLayoutData> LayoutActors;
TSet<AActor*> Seen;
for (AActor* Actor : Actors)
{
if (!Actor || Seen.Contains(Actor))
{
continue;
}
Seen.Add(Actor);
if (!IsLayoutActorValid(Actor))
{
Result.SkippedActors.Add(Actor);
continue;
}
FActorLayoutData Data;
Data.Actor = Actor;
Data.Location = Actor->GetActorLocation();
if (!IsFiniteVector(Data.Location)
|| !IsFiniteVector(Actor->GetActorScale3D())
|| !IsFiniteQuat(Actor->GetActorQuat()))
{
Result.SkippedActors.Add(Actor);
continue;
}
Data.Bounds = Actor->GetComponentsBoundingBox(true, true);
if (!Data.Bounds.IsValid)
{
Data.Bounds = FBox(Data.Location, Data.Location);
}
if (!IsFiniteVector(Data.Bounds.Min) || !IsFiniteVector(Data.Bounds.Max))
{
Result.SkippedActors.Add(Actor);
continue;
}
Data.AttachmentDepth = GetAttachmentDepth(Actor);
LayoutActors.Add(MoveTemp(Data));
}
return LayoutActors;
}
void ApplyLocations(
TArray<FActorLayoutData>& LayoutActors,
const TMap<AActor*, FVector>& Locations,
FDirectiveUtilActorOperationResult& Result)
{
LayoutActors.Sort([](const FActorLayoutData& Left, const FActorLayoutData& Right) {
return Left.AttachmentDepth < Right.AttachmentDepth;
});
for (const FActorLayoutData& Data : LayoutActors)
{
const FVector* Location = Locations.Find(Data.Actor);
if (!Location || !IsFiniteVector(*Location) || Data.Actor->GetActorLocation().Equals(*Location))
{
continue;
}
Data.Actor->Modify();
if (Data.Actor->SetActorLocation(*Location, false, nullptr, ETeleportType::TeleportPhysics))
{
Result.ChangedActors.Add(Data.Actor);
}
else
{
Result.SkippedActors.Add(Data.Actor);
}
}
}
}
void UDirectiveUtilEditorActorSubsystem::FocusActorsInViewport(const TArray<AActor*> Actors, const bool bInstant)
{
if (Actors.Num() == 0) { return; }
if (!GEditor) { return; }
FViewport* ActiveViewport = GEditor->GetActiveViewport();
if (!ActiveViewport) { return; }
FEditorViewportClient* ViewportClient = static_cast<FEditorViewportClient*>(ActiveViewport->GetClient());
if (!ViewportClient) { return; }
if (!GCurrentLevelEditingViewportClient) { return; }
FBox BoundingBox = FBox(ForceInit);
for (const auto Actor : Actors)
for (const AActor* Actor : Actors)
{
if (!Actor) { continue; }
BoundingBox += Actor->GetComponentsBoundingBox(true, true);
if (!IsValid(Actor)) { continue; }
const FBox ActorBounds = Actor->GetComponentsBoundingBox(true, true);
if (ActorBounds.IsValid)
{
BoundingBox += ActorBounds;
}
}
ViewportClient->FocusViewportOnBox(BoundingBox, bInstant);
if (BoundingBox.IsValid)
{
GCurrentLevelEditingViewportClient->FocusViewportOnBox(BoundingBox, bInstant);
}
}
TArray<UClass*> UDirectiveUtilEditorActorSubsystem::GetAllLevelClasses()
@@ -84,6 +250,236 @@ TArray<UClass*> UDirectiveUtilEditorActorSubsystem::GetAllLevelClasses()
return ActorClasses;
}
FDirectiveUtilActorOperationResult UDirectiveUtilEditorActorSubsystem::AlignActors(
const TArray<AActor*>& Actors,
const EDirectiveUtilActorLayoutAxis Axis,
const EDirectiveUtilActorAlignment Alignment)
{
FDirectiveUtilActorOperationResult Result;
TArray<FActorLayoutData> LayoutActors = GetLayoutActors(Actors, Result);
if (LayoutActors.Num() < 2)
{
return Result;
}
FBox CombinedBounds(ForceInit);
for (const FActorLayoutData& Data : LayoutActors)
{
CombinedBounds += Data.Bounds;
}
float Target = 0.0f;
switch (Alignment)
{
case EDirectiveUtilActorAlignment::Minimum:
Target = GetAxisValue(CombinedBounds.Min, Axis);
break;
case EDirectiveUtilActorAlignment::Center:
Target = GetAxisValue(CombinedBounds.GetCenter(), Axis);
break;
case EDirectiveUtilActorAlignment::Maximum:
Target = GetAxisValue(CombinedBounds.Max, Axis);
break;
}
TMap<AActor*, FVector> Locations;
for (const FActorLayoutData& Data : LayoutActors)
{
const float Current = Alignment == EDirectiveUtilActorAlignment::Minimum
? GetAxisValue(Data.Bounds.Min, Axis)
: Alignment == EDirectiveUtilActorAlignment::Maximum
? GetAxisValue(Data.Bounds.Max, Axis)
: GetAxisValue(Data.Bounds.GetCenter(), Axis);
FVector Location = Data.Location;
SetAxisValue(Location, Axis, GetAxisValue(Location, Axis) + Target - Current);
Locations.Add(Data.Actor, Location);
}
const FScopedTransaction Transaction(NSLOCTEXT("DirectiveUtilities", "AlignActors", "Align Actors"));
ApplyLocations(LayoutActors, Locations, Result);
return Result;
}
FDirectiveUtilActorOperationResult UDirectiveUtilEditorActorSubsystem::DistributeActors(
const TArray<AActor*>& Actors,
const EDirectiveUtilActorLayoutAxis Axis,
const EDirectiveUtilActorDistribution Distribution)
{
FDirectiveUtilActorOperationResult Result;
TArray<FActorLayoutData> LayoutActors = GetLayoutActors(Actors, Result);
if (LayoutActors.Num() < 3)
{
return Result;
}
LayoutActors.Sort([Axis](const FActorLayoutData& Left, const FActorLayoutData& Right) {
return GetAxisValue(Left.Bounds.GetCenter(), Axis) < GetAxisValue(Right.Bounds.GetCenter(), Axis);
});
TMap<AActor*, FVector> Locations;
if (Distribution == EDirectiveUtilActorDistribution::Centers)
{
const float Start = GetAxisValue(LayoutActors[0].Bounds.GetCenter(), Axis);
const float End = GetAxisValue(LayoutActors.Last().Bounds.GetCenter(), Axis);
const float Spacing = (End - Start) / (LayoutActors.Num() - 1);
for (int32 Index = 1; Index < LayoutActors.Num() - 1; ++Index)
{
const FActorLayoutData& Data = LayoutActors[Index];
FVector Location = Data.Location;
const float Delta = Start + Spacing * Index - GetAxisValue(Data.Bounds.GetCenter(), Axis);
SetAxisValue(Location, Axis, GetAxisValue(Location, Axis) + Delta);
Locations.Add(Data.Actor, Location);
}
}
else
{
float TotalSize = 0.0f;
for (const FActorLayoutData& Data : LayoutActors)
{
TotalSize += GetAxisValue(Data.Bounds.GetSize(), Axis);
}
const float Start = GetAxisValue(LayoutActors[0].Bounds.Min, Axis);
const float End = GetAxisValue(LayoutActors.Last().Bounds.Max, Axis);
const float Gap = (End - Start - TotalSize) / (LayoutActors.Num() - 1);
float NextMinimum = GetAxisValue(LayoutActors[0].Bounds.Max, Axis) + Gap;
for (int32 Index = 1; Index < LayoutActors.Num() - 1; ++Index)
{
const FActorLayoutData& Data = LayoutActors[Index];
FVector Location = Data.Location;
const float Delta = NextMinimum - GetAxisValue(Data.Bounds.Min, Axis);
SetAxisValue(Location, Axis, GetAxisValue(Location, Axis) + Delta);
Locations.Add(Data.Actor, Location);
NextMinimum += GetAxisValue(Data.Bounds.GetSize(), Axis) + Gap;
}
}
const FScopedTransaction Transaction(NSLOCTEXT("DirectiveUtilities", "DistributeActors", "Distribute Actors"));
ApplyLocations(LayoutActors, Locations, Result);
return Result;
}
FDirectiveUtilActorOperationResult UDirectiveUtilEditorActorSubsystem::SnapActorsToSurface(
const TArray<AActor*>& Actors,
const FVector TraceDirection,
const float MaximumDistance,
const TEnumAsByte<ECollisionChannel> TraceChannel,
const EDirectiveUtilSurfacePlacement Placement,
const bool bAlignToNormal)
{
FDirectiveUtilActorOperationResult Result;
TArray<FActorLayoutData> LayoutActors = GetLayoutActors(Actors, Result);
if (LayoutActors.IsEmpty()
|| !FMath::IsFinite(MaximumDistance)
|| MaximumDistance <= 0.0f
|| !IsFiniteVector(TraceDirection)
|| TraceDirection.IsNearlyZero()
|| TraceChannel.GetValue() >= ECC_MAX)
{
for (const FActorLayoutData& Data : LayoutActors)
{
Result.SkippedActors.Add(Data.Actor);
}
return Result;
}
const FVector Direction = TraceDirection.GetSafeNormal();
FCollisionQueryParams QueryParams(SCENE_QUERY_STAT(DirectiveUtilitiesSnapActors), true);
for (const FActorLayoutData& Data : LayoutActors)
{
QueryParams.AddIgnoredActor(Data.Actor);
}
struct FSnapTarget
{
FVector Location;
FQuat Rotation;
};
TMap<AActor*, FSnapTarget> Targets;
for (const FActorLayoutData& Data : LayoutActors)
{
FHitResult Hit;
const FVector Start = Data.Location;
if (!Data.Actor->GetWorld()->LineTraceSingleByChannel(
Hit,
Start,
Start + Direction * MaximumDistance,
TraceChannel,
QueryParams))
{
Result.SkippedActors.Add(Data.Actor);
continue;
}
if (!IsFiniteVector(Hit.Location)
|| (bAlignToNormal && (!IsFiniteVector(Hit.ImpactNormal) || Hit.ImpactNormal.IsNearlyZero())))
{
Result.SkippedActors.Add(Data.Actor);
continue;
}
FVector Location = Hit.Location;
if (Placement == EDirectiveUtilSurfacePlacement::Bounds)
{
const FVector Extent = Data.Bounds.GetExtent();
const float Support = FMath::Abs(Direction.X) * Extent.X
+ FMath::Abs(Direction.Y) * Extent.Y
+ FMath::Abs(Direction.Z) * Extent.Z;
Location = Hit.Location - Direction * Support - (Data.Bounds.GetCenter() - Data.Location);
}
FQuat Rotation = Data.Actor->GetActorQuat();
if (bAlignToNormal)
{
Rotation = FQuat::FindBetweenNormals(Rotation.GetUpVector(), Hit.ImpactNormal) * Rotation;
Rotation.Normalize();
}
if (IsFiniteVector(Location) && IsFiniteQuat(Rotation))
{
Targets.Add(Data.Actor, {Location, Rotation});
}
else
{
Result.SkippedActors.Add(Data.Actor);
}
}
LayoutActors.Sort([](const FActorLayoutData& Left, const FActorLayoutData& Right) {
return Left.AttachmentDepth < Right.AttachmentDepth;
});
const FScopedTransaction Transaction(NSLOCTEXT("DirectiveUtilities", "SnapActorsToSurface", "Snap Actors To Surface"));
for (const FActorLayoutData& Data : LayoutActors)
{
const FSnapTarget* Target = Targets.Find(Data.Actor);
if (!Target)
{
continue;
}
const bool bLocationChanged = !Data.Actor->GetActorLocation().Equals(Target->Location);
const bool bRotationChanged = !Data.Actor->GetActorQuat().Equals(Target->Rotation);
if (!bLocationChanged && !bRotationChanged)
{
continue;
}
Data.Actor->Modify();
const bool bMoved = Data.Actor->SetActorLocationAndRotation(
Target->Location,
Target->Rotation,
false,
nullptr,
ETeleportType::TeleportPhysics);
if (bMoved)
{
Result.ChangedActors.Add(Data.Actor);
}
else
{
Result.SkippedActors.Add(Data.Actor);
}
}
return Result;
}
void UDirectiveUtilEditorActorSubsystem::FilterStaticMeshActors(
TArray<AStaticMeshActor*>& OutStaticMeshActors,
TArray<AActor*> ActorsToFilter) const
@@ -735,11 +1131,11 @@ void UDirectiveUtilEditorActorSubsystem::FilterActorsByTexture(
{
if (const UMaterialExpressionTextureSample* TextureSample = Cast<UMaterialExpressionTextureSample>(Expression))
{
if (TextureSample->Texture == TextureReference) { return true; }
if (MatchesTextureReference(TextureSample->Texture, TextureReference)) { return true; }
}
if (const UMaterialExpressionTextureObject* TextureObject = Cast<UMaterialExpressionTextureObject>(Expression))
{
if (TextureObject->Texture == TextureReference) { return true; }
if (MatchesTextureReference(TextureObject->Texture, TextureReference)) { return true; }
}
}
}
@@ -757,11 +1153,11 @@ void UDirectiveUtilEditorActorSubsystem::FilterActorsByTexture(
{
if (const UMaterialExpressionTextureSample* TextureSample = Cast<UMaterialExpressionTextureSample>(Expression))
{
if (TextureSample->Texture == TextureReference) { return true; }
if (MatchesTextureReference(TextureSample->Texture, TextureReference)) { return true; }
}
if (const UMaterialExpressionTextureObject* TextureObject = Cast<UMaterialExpressionTextureObject>(Expression))
{
if (TextureObject->Texture == TextureReference) { return true; }
if (MatchesTextureReference(TextureObject->Texture, TextureReference)) { return true; }
}
}
}
@@ -901,36 +1297,7 @@ void UDirectiveUtilEditorActorSubsystem::GetActorsByMaterialSoftReference(
const EDirectiveUtilInclusivity Inclusivity)
{
const TArray<AActor*> SourceActors = SelectionMethod == Selection ? GetSelectedLevelActors() : GetAllLevelActors();
TArray<AStaticMeshActor*> StaticMeshActors;
FilterStaticMeshActors(StaticMeshActors, SourceActors);
FilterActorsByPredicate(StaticMeshActors, FoundActors, Inclusivity, [&](AStaticMeshActor* StaticMeshActor) -> bool
{
const UStaticMeshComponent* StaticMeshComp = StaticMeshActor->GetStaticMeshComponent();
if (!StaticMeshComp) { return false; }
const UStaticMesh* Mesh = StaticMeshComp->GetStaticMesh();
if (!Mesh) { return false; }
if (MaterialSource == BaseAndOverride || MaterialSource == OverrideOnly)
{
for (int32 i = 0; i < StaticMeshComp->GetNumMaterials(); i++)
{
if (StaticMeshComp->GetMaterial(i) == Material) { return true; }
}
}
if (MaterialSource == BaseAndOverride || MaterialSource == BaseOnly)
{
for (int32 i = 0; i < Mesh->GetStaticMaterials().Num(); i++)
{
if (Mesh->GetMaterial(i) == Material) { return true; }
}
}
return false;
});
UE_LOG(LogDirectiveUtilEditor, Display, TEXT("%i actors with the material reference were found."),
FoundActors.Num());
FilterActorsByMaterial(SourceActors, FoundActors, Material, MaterialSource, Inclusivity);
}
void UDirectiveUtilEditorActorSubsystem::GetActorsByMaterialName(
@@ -941,38 +1308,7 @@ void UDirectiveUtilEditorActorSubsystem::GetActorsByMaterialName(
const EDirectiveUtilInclusivity Inclusivity)
{
const TArray<AActor*> SourceActors = SelectionMethod == Selection ? GetSelectedLevelActors() : GetAllLevelActors();
TArray<AStaticMeshActor*> StaticMeshActors;
FilterStaticMeshActors(StaticMeshActors, SourceActors);
FilterActorsByPredicate(StaticMeshActors, FoundActors, Inclusivity, [&](AStaticMeshActor* StaticMeshActor) -> bool
{
const UStaticMeshComponent* StaticMeshComp = StaticMeshActor->GetStaticMeshComponent();
if (!StaticMeshComp) { return false; }
const UStaticMesh* Mesh = StaticMeshComp->GetStaticMesh();
if (!Mesh) { return false; }
if (MaterialSource == BaseAndOverride || MaterialSource == OverrideOnly)
{
for (int32 i = 0; i < StaticMeshComp->GetNumMaterials(); i++)
{
const UMaterialInterface* Mat = StaticMeshComp->GetMaterial(i);
if (Mat && Mat->GetName().Contains(MaterialName)) { return true; }
}
}
if (MaterialSource == BaseAndOverride || MaterialSource == BaseOnly)
{
for (int32 i = 0; i < Mesh->GetStaticMaterials().Num(); i++)
{
const UMaterialInterface* Mat = Mesh->GetMaterial(i);
if (Mat && Mat->GetName().Contains(MaterialName)) { return true; }
}
}
return false;
});
UE_LOG(LogDirectiveUtilEditor, Display, TEXT("%i actors with material %s were found."), FoundActors.Num(),
*MaterialName);
FilterActorsByMaterialName(SourceActors, FoundActors, MaterialName, MaterialSource, Inclusivity);
}
void UDirectiveUtilEditorActorSubsystem::GetActorsByVertexCount(
@@ -1288,11 +1624,11 @@ void UDirectiveUtilEditorActorSubsystem::GetActorsByTextureSoftReference(
{
if (const UMaterialExpressionTextureSample* TextureSample = Cast<UMaterialExpressionTextureSample>(Expression))
{
if (TextureSample->Texture == Texture) { return true; }
if (MatchesTextureReference(TextureSample->Texture, Texture)) { return true; }
}
if (const UMaterialExpressionTextureObject* TextureObject = Cast<UMaterialExpressionTextureObject>(Expression))
{
if (TextureObject->Texture == Texture) { return true; }
if (MatchesTextureReference(TextureObject->Texture, Texture)) { return true; }
}
}
}
@@ -1341,8 +1677,7 @@ void UDirectiveUtilEditorActorSubsystem::GetActorsByTextureName(
void UDirectiveUtilEditorActorSubsystem::GetInvalidActors(TArray<AActor*>& FoundActors)
{
for (AActor* Actor : GetAllLevelActors()) { if (!IsValid(Actor)) { FoundActors.AddUnique(Actor); } }
UE_LOG(LogDirectiveUtilEditor, Display, TEXT("%i invalid actors were found."), FoundActors.Num());
FoundActors.Reset();
}
void UDirectiveUtilEditorActorSubsystem::PushOverrideMaterialsToSource(UStaticMeshComponent* StaticMeshComponent)
@@ -1361,11 +1696,14 @@ void UDirectiveUtilEditorActorSubsystem::PushOverrideMaterialsToSource(UStaticMe
}
const FScopedTransaction Transaction(NSLOCTEXT("DirectiveUtilities", "PushOverrideMaterialsToSource", "Push Override Materials To Source"));
for (int32 i = 0; i < StaticMeshComponent->GetNumMaterials(); i++)
const TArray<TObjectPtr<UMaterialInterface>>& OverrideMaterials = StaticMeshComponent->OverrideMaterials;
const int32 MaterialSlotCount = StaticMesh->GetStaticMaterials().Num();
for (int32 MaterialIndex = 0; MaterialIndex < FMath::Min(OverrideMaterials.Num(), MaterialSlotCount); ++MaterialIndex)
{
if (UMaterialInterface* Material = StaticMeshComponent->GetMaterial(i))
UMaterialInterface* OverrideMaterial = OverrideMaterials[MaterialIndex];
if (OverrideMaterial && StaticMesh->GetMaterial(MaterialIndex) != OverrideMaterial)
{
StaticMesh->SetMaterial(i, Material);
StaticMesh->SetMaterial(MaterialIndex, OverrideMaterial);
}
}
UE_LOG(LogDirectiveUtilEditor, Display, TEXT("Materials were pushed to source for %s."), *StaticMeshComponent->GetName());

View File

@@ -0,0 +1,85 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Tasks/DirectiveUtilEditorSlowTask.h"
#include "Misc/ScopedSlowTask.h"
namespace
{
UDirectiveUtilEditorSlowTask* ActiveSlowTask = nullptr;
}
void UDirectiveUtilEditorSlowTask::Initialize(
const float TotalWork,
const FText& Description,
const bool bCanCancel,
const bool bShowDialog)
{
Finish();
if (!FMath::IsFinite(TotalWork) || TotalWork <= 0.0f)
{
return;
}
if (ActiveSlowTask && ActiveSlowTask != this)
{
return;
}
SlowTask = new FScopedSlowTask(TotalWork, Description);
ActiveSlowTask = this;
AddToRoot();
if (bShowDialog)
{
SlowTask->MakeDialog(bCanCancel, true);
}
}
bool UDirectiveUtilEditorSlowTask::Advance(const float Work, const FText& Message)
{
if (!SlowTask || !FMath::IsFinite(Work) || Work < 0.0f)
{
return false;
}
const float CompletedWork = SlowTask->CompletedWork + SlowTask->CurrentFrameScope;
const float RemainingWork = FMath::Max(0.0f, SlowTask->TotalAmountOfWork - CompletedWork);
SlowTask->EnterProgressFrame(FMath::Min(Work, RemainingWork), Message);
return true;
}
bool UDirectiveUtilEditorSlowTask::IsCancelRequested() const
{
return SlowTask && SlowTask->ShouldCancel();
}
bool UDirectiveUtilEditorSlowTask::IsActive() const
{
return SlowTask != nullptr;
}
void UDirectiveUtilEditorSlowTask::Finish()
{
if (!SlowTask)
{
return;
}
delete SlowTask;
SlowTask = nullptr;
if (ActiveSlowTask == this)
{
ActiveSlowTask = nullptr;
}
if (IsRooted())
{
RemoveFromRoot();
}
}
void UDirectiveUtilEditorSlowTask::FinishActiveTask()
{
if (ActiveSlowTask)
{
ActiveSlowTask->Finish();
}
}

View File

@@ -0,0 +1,36 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Types/DirectiveUtilEditorAuditTypes.h"
#include "DirectiveUtilEditorAssetAuditLibrary.generated.h"
UCLASS()
class DIRECTIVEUTILITIESEDITOR_API UDirectiveUtilEditorAssetAuditLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/** Returns assets with no known Asset Registry referencers. Results are candidates and may still be loaded indirectly. */
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Asset Audit")
static TArray<FAssetData> FindUnreferencedAssetCandidates(const FDirectiveUtilAssetAuditOptions& Options);
/** Returns Asset Registry dependencies whose packages cannot be found. */
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Asset Audit")
static TArray<FDirectiveUtilMissingAssetReference> FindMissingAssetReferences(const FDirectiveUtilAssetAuditOptions& Options);
/** Returns dependency cycles between assets in the scanned paths. */
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Asset Audit")
static TArray<FDirectiveUtilAssetDependencyCycle> FindAssetDependencyCycles(const FDirectiveUtilAssetAuditOptions& Options);
/** Builds a read-only report for assets in the scanned paths. */
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Asset Audit")
static FDirectiveUtilAssetAuditReport BuildAssetAuditReport(const FDirectiveUtilAssetAuditOptions& Options);
/** Converts an asset audit report to CSV text. */
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Editor|Asset Audit")
static FString AssetAuditReportToCsv(const FDirectiveUtilAssetAuditReport& Report);
};

View File

@@ -2,6 +2,7 @@
#pragma once
#include "AssetRegistry/AssetData.h"
#include "CoreMinimal.h"
#include "EditorAssetLibrary.h"
#include "Types/DirectiveUtilEditorAssetTypes.h"
@@ -44,7 +45,8 @@ public:
/**
* Fixes up (and deletes) object redirectors found under the given directories, without loading every asset.
* Equivalent to the Content Browser's "Fix Up Redirectors in Folder", but scriptable and headless-friendly.
* Equivalent to the Content Browser's "Fix Up Redirectors in Folder". Unreal shows a completion dialog,
* so this returns Failure in unattended sessions instead of blocking the process.
* @param DirectoryPaths Directories to scan for redirectors. If empty, the entire registry is scanned.
* @param OutRedirectorsProcessed [out] The number of redirectors submitted for fix-up (the engine does not report per-redirector success).
* @return Success if the operation ran (even if nothing needed fixing), Failure otherwise (e.g. a fixup is already in progress).

View File

@@ -0,0 +1,49 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "AssetRegistry/AssetData.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Types/DirectiveUtilEditorBlueprintTypes.h"
#include "DirectiveUtilEditorBlueprintLibrary.generated.h"
class UBlueprint;
UCLASS()
class DIRECTIVEUTILITIESEDITOR_API UDirectiveUtilEditorBlueprintLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Editor|Blueprint Inspection")
static EDirectiveUtilBlueprintCompileStatus GetBlueprintCompileStatus(const UBlueprint* Blueprint);
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Blueprint Inspection")
static TArray<FAssetData> FindBlueprintsByCompileStatus(
EDirectiveUtilBlueprintCompileStatus CompileStatus,
const FDirectiveUtilBlueprintSearchOptions& Options);
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Blueprint Inspection")
static TArray<FAssetData> FindBlueprintsByParentClass(
UClass* ParentClass,
const FDirectiveUtilBlueprintSearchOptions& Options,
bool bIncludeDescendants = true);
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Blueprint Inspection")
static TArray<FAssetData> FindBlueprintsImplementingInterface(
UClass* InterfaceClass,
const FDirectiveUtilBlueprintSearchOptions& Options);
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Blueprint Inspection")
static TArray<FAssetData> FindBlueprintsContainingComponentClass(
UClass* ComponentClass,
const FDirectiveUtilBlueprintSearchOptions& Options,
bool bIncludeDerivedComponents = true);
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Editor|Blueprint Inspection")
static TArray<FName> GetUnusedBlueprintVariables(
UBlueprint* Blueprint,
bool bIncludeExternallyAccessibleVariables = false);
};

View File

@@ -0,0 +1,25 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Types/DirectiveUtilEditorTaskTypes.h"
#include "DirectiveUtilEditorTaskLibrary.generated.h"
class UDirectiveUtilEditorSlowTask;
UCLASS()
class DIRECTIVEUTILITIESEDITOR_API UDirectiveUtilEditorTaskLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/** Starts a modal editor progress task. TotalWork must be greater than zero, and only one task can be active. */
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Task")
static UDirectiveUtilEditorSlowTask* StartEditorSlowTask(float TotalWork, const FText& Description, bool bCanCancel = false);
/** Displays a fire-and-forget editor notification. */
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Notification")
static bool ShowEditorNotification(const FText& Message, EDirectiveUtilEditorNotificationState State = EDirectiveUtilEditorNotificationState::Neutral, float ExpireDuration = 3.0f);
};

View File

@@ -11,7 +11,7 @@
class UCapsuleComponent;
class UBoxComponent;
class USphereComponent;
class UStaticMeshActor;
class AStaticMeshActor;
/**
* DirectiveUtilEditorActorSubsystem
@@ -44,6 +44,30 @@ public:
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor")
TArray<UClass*> GetAllLevelClasses();
/** Aligns actor bounds to the combined minimum, center, or maximum on one axis. */
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Actor Layout")
static FDirectiveUtilActorOperationResult AlignActors(
const TArray<AActor*>& Actors,
EDirectiveUtilActorLayoutAxis Axis,
EDirectiveUtilActorAlignment Alignment);
/** Distributes actors between the outer actors using equal center spacing or equal bounds gaps. */
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Actor Layout")
static FDirectiveUtilActorOperationResult DistributeActors(
const TArray<AActor*>& Actors,
EDirectiveUtilActorLayoutAxis Axis,
EDirectiveUtilActorDistribution Distribution);
/** Traces from each actor and places its pivot or directional bounds on the hit surface. */
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Actor Layout", meta = (AdvancedDisplay = "bAlignToNormal"))
static FDirectiveUtilActorOperationResult SnapActorsToSurface(
const TArray<AActor*>& Actors,
FVector TraceDirection,
float MaximumDistance,
TEnumAsByte<ECollisionChannel> TraceChannel = ECC_Visibility,
EDirectiveUtilSurfacePlacement Placement = EDirectiveUtilSurfacePlacement::Bounds,
bool bAlignToNormal = false);
//-----------------------------
// Filters
//-----------------------------
@@ -679,7 +703,7 @@ public:
* Returns a list of invalid actors.
* @param FoundActors The list of actors that were found.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(AdvancedDisplay=1))
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(DeprecatedFunction, DeprecationMessage="Actor enumeration excludes invalid actors. This node always returns an empty array."))
void GetInvalidActors(TArray<AActor*>& FoundActors);
//-----------------------------

View File

@@ -0,0 +1,33 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "DirectiveUtilEditorSlowTask.generated.h"
struct FScopedSlowTask;
UCLASS(BlueprintType)
class DIRECTIVEUTILITIESEDITOR_API UDirectiveUtilEditorSlowTask : public UObject
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Task")
bool Advance(float Work, const FText& Message);
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Editor|Task")
bool IsCancelRequested() const;
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Editor|Task")
bool IsActive() const;
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Task")
void Finish();
void Initialize(float TotalWork, const FText& Description, bool bCanCancel, bool bShowDialog);
static void FinishActiveTask();
private:
FScopedSlowTask* SlowTask = nullptr;
};

View File

@@ -0,0 +1,112 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "AssetRegistry/AssetData.h"
#include "DirectiveUtilEditorAuditTypes.generated.h"
UENUM(BlueprintType)
enum class EDirectiveUtilAssetDependencyType : uint8
{
HardPackage UMETA(DisplayName = "Hard Package"),
SoftPackage UMETA(DisplayName = "Soft Package"),
SearchableName UMETA(DisplayName = "Searchable Name"),
DirectManagement UMETA(DisplayName = "Direct Management"),
IndirectManagement UMETA(DisplayName = "Indirect Management"),
};
USTRUCT(BlueprintType)
struct FDirectiveUtilAssetAuditOptions
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Directive Utilities|Asset Audit")
TArray<FName> PackagePaths;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Directive Utilities|Asset Audit")
TArray<FName> ExcludedPackagePaths;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Directive Utilities|Asset Audit")
bool bUseDefaultPathExclusions = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Directive Utilities|Asset Audit")
bool bIncludePrimaryAssets = false;
FDirectiveUtilAssetAuditOptions()
{
PackagePaths.Add(TEXT("/Game"));
}
};
USTRUCT(BlueprintType)
struct FDirectiveUtilMissingAssetReference
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
FAssetData ReferencingAsset;
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
FName MissingPackage;
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
EDirectiveUtilAssetDependencyType DependencyType = EDirectiveUtilAssetDependencyType::SoftPackage;
};
USTRUCT(BlueprintType)
struct FDirectiveUtilAssetDependencyCycle
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
TArray<FName> Packages;
};
USTRUCT(BlueprintType)
struct FDirectiveUtilAssetAuditEntry
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
FAssetData Asset;
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
FName PackageName;
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
FName PackagePath;
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
FString AssetClass;
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
int64 DiskSize = 0;
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
int32 DependencyCount = 0;
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
int32 ReferencerCount = 0;
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
bool bPrimaryAsset = false;
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
TArray<FString> Findings;
};
USTRUCT(BlueprintType)
struct FDirectiveUtilAssetAuditReport
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
TArray<FDirectiveUtilAssetAuditEntry> Assets;
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
TArray<FDirectiveUtilMissingAssetReference> MissingReferences;
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
TArray<FDirectiveUtilAssetDependencyCycle> DependencyCycles;
};

View File

@@ -0,0 +1,34 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "DirectiveUtilEditorBlueprintTypes.generated.h"
UENUM(BlueprintType)
enum class EDirectiveUtilBlueprintCompileStatus : uint8
{
Unknown,
Dirty,
Error,
UpToDate UMETA(DisplayName = "Up To Date"),
BeingCreated UMETA(DisplayName = "Being Created"),
UpToDateWithWarnings UMETA(DisplayName = "Up To Date With Warnings"),
};
USTRUCT(BlueprintType)
struct FDirectiveUtilBlueprintSearchOptions
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Directive Utilities|Blueprint Inspection")
TArray<FName> PackagePaths;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Directive Utilities|Blueprint Inspection")
TArray<FName> ExcludedPackagePaths;
FDirectiveUtilBlueprintSearchOptions()
{
PackagePaths.Add(TEXT("/Game"));
}
};

View File

@@ -0,0 +1,14 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "DirectiveUtilEditorTaskTypes.generated.h"
UENUM(BlueprintType)
enum class EDirectiveUtilEditorNotificationState : uint8
{
Neutral,
Success,
Warning,
Failure
};

View File

@@ -3,6 +3,51 @@
#pragma once
#include "CoreMinimal.h"
#include "DirectiveUtilEditorTypes.generated.h"
class AActor;
UENUM(BlueprintType)
enum class EDirectiveUtilActorLayoutAxis : uint8
{
X,
Y,
Z,
};
UENUM(BlueprintType)
enum class EDirectiveUtilActorAlignment : uint8
{
Minimum,
Center,
Maximum,
};
UENUM(BlueprintType)
enum class EDirectiveUtilActorDistribution : uint8
{
Centers,
BoundsGaps UMETA(DisplayName = "Bounds Gaps"),
};
UENUM(BlueprintType)
enum class EDirectiveUtilSurfacePlacement : uint8
{
Pivot,
Bounds,
};
USTRUCT(BlueprintType)
struct FDirectiveUtilActorOperationResult
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Actor Layout")
TArray<TObjectPtr<AActor>> ChangedActors;
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Actor Layout")
TArray<TObjectPtr<AActor>> SkippedActors;
};
/**
* EDirectiveUtilSelectionMethod