Import helper plugin

This commit is contained in:
2026-07-15 19:17:47 +02:00
parent fd992725a2
commit 3c084d9669
72 changed files with 11853 additions and 1 deletions

View File

@@ -0,0 +1,31 @@
[CoreRedirects]
; Redirects Blueprint assets saved with the 1.x class, struct, enum, and package names.
+PackageRedirects=(OldName="/Script/UDCore",NewName="/Script/DirectiveUtilitiesRuntime")
+PackageRedirects=(OldName="/Script/UDCoreEditor",NewName="/Script/DirectiveUtilitiesEditor")
+ClassRedirects=(OldName="/Script/UDCore.UDCoreArrayFunctionLibrary",NewName="/Script/DirectiveUtilitiesRuntime.DirectiveUtilArrayFunctionLibrary")
+ClassRedirects=(OldName="/Script/UDCore.UDCoreFunctionLibrary",NewName="/Script/DirectiveUtilitiesRuntime.DirectiveUtilFunctionLibrary")
+ClassRedirects=(OldName="/Script/UDCore.UDCoreGameplayTagFunctionLibrary",NewName="/Script/DirectiveUtilitiesRuntime.DirectiveUtilGameplayTagFunctionLibrary")
+ClassRedirects=(OldName="/Script/UDCore.UDCoreInputFunctionLibrary",NewName="/Script/DirectiveUtilitiesRuntime.DirectiveUtilInputFunctionLibrary")
+ClassRedirects=(OldName="/Script/UDCore.UDCoreMapFunctionLibrary",NewName="/Script/DirectiveUtilitiesRuntime.DirectiveUtilMapFunctionLibrary")
+ClassRedirects=(OldName="/Script/UDCore.UDCoreMathFunctionLibrary",NewName="/Script/DirectiveUtilitiesRuntime.DirectiveUtilMathFunctionLibrary")
+ClassRedirects=(OldName="/Script/UDCore.UDCoreRegexFunctionLibrary",NewName="/Script/DirectiveUtilitiesRuntime.DirectiveUtilRegexFunctionLibrary")
+ClassRedirects=(OldName="/Script/UDCore.UDCoreSaveGameFunctionLibrary",NewName="/Script/DirectiveUtilitiesRuntime.DirectiveUtilSaveGameFunctionLibrary")
+ClassRedirects=(OldName="/Script/UDCore.UDCoreStringFunctionLibrary",NewName="/Script/DirectiveUtilitiesRuntime.DirectiveUtilStringFunctionLibrary")
+ClassRedirects=(OldName="/Script/UDCore.UDCoreTextFunctionLibrary",NewName="/Script/DirectiveUtilitiesRuntime.DirectiveUtilTextFunctionLibrary")
+ClassRedirects=(OldName="/Script/UDCore.UDAT_Delay",NewName="/Script/DirectiveUtilitiesRuntime.DirectiveUtilTask_Delay")
+ClassRedirects=(OldName="/Script/UDCore.UDAT_AsyncLoadAsset",NewName="/Script/DirectiveUtilitiesRuntime.DirectiveUtilTask_AsyncLoadAsset")
+ClassRedirects=(OldName="/Script/UDCore.UDAT_AsyncLoadClass",NewName="/Script/DirectiveUtilitiesRuntime.DirectiveUtilTask_AsyncLoadClass")
+ClassRedirects=(OldName="/Script/UDCore.UDAT_AsyncLoadAssets",NewName="/Script/DirectiveUtilitiesRuntime.DirectiveUtilTask_AsyncLoadAssets")
+ClassRedirects=(OldName="/Script/UDCore.UDAT_AsyncTrace",NewName="/Script/DirectiveUtilitiesRuntime.DirectiveUtilTask_AsyncTrace")
+ClassRedirects=(OldName="/Script/UDCore.UDAT_MoveToLocation",NewName="/Script/DirectiveUtilitiesRuntime.DirectiveUtilTask_MoveToLocation")
+ClassRedirects=(OldName="/Script/UDCore.UDAT_MoveToActor",NewName="/Script/DirectiveUtilitiesRuntime.DirectiveUtilTask_MoveToActor")
+ClassRedirects=(OldName="/Script/UDCoreEditor.UDCoreEditorActorSubsystem",NewName="/Script/DirectiveUtilitiesEditor.DirectiveUtilEditorActorSubsystem")
+ClassRedirects=(OldName="/Script/UDCoreEditor.UDCoreEditorAssetLibrary",NewName="/Script/DirectiveUtilitiesEditor.DirectiveUtilEditorAssetLibrary")
+StructRedirects=(OldName="/Script/UDCore.UDCoreEnhancedInputContextData",NewName="/Script/DirectiveUtilitiesRuntime.DirectiveUtilEnhancedInputContextData")
+StructRedirects=(OldName="/Script/UDCoreEditor.UDAssetKey",NewName="/Script/DirectiveUtilitiesEditor.DirectiveUtilAssetKey")
+StructRedirects=(OldName="/Script/UDCoreEditor.UDDuplicateAssetData",NewName="/Script/DirectiveUtilitiesEditor.DirectiveUtilDuplicateAssetData")
+EnumRedirects=(OldName="/Script/UDCore.EUDEaseType",NewName="/Script/DirectiveUtilitiesRuntime.EDirectiveUtilEaseType")
+EnumRedirects=(OldName="/Script/UDCore.EUDSuccessStatus",NewName="/Script/DirectiveUtilitiesRuntime.EDirectiveUtilSuccessStatus")
+EnumRedirects=(OldName="/Script/UDCoreEditor.EUDSelectionMethod",NewName="/Script/DirectiveUtilitiesEditor.EDirectiveUtilSelectionMethod")
+EnumRedirects=(OldName="/Script/UDCoreEditor.EUDInclusivity",NewName="/Script/DirectiveUtilitiesEditor.EDirectiveUtilInclusivity")
+EnumRedirects=(OldName="/Script/UDCoreEditor.EUDSearchLocation",NewName="/Script/DirectiveUtilitiesEditor.EDirectiveUtilSearchLocation")

View File

@@ -0,0 +1,13 @@
[FilterPlugin]
; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and
; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively.
;
; Examples:
; /README.txt
; /Extras/...
; /Binaries/ThirdParty/*.dll
/README.md
/LICENSE
/CHANGELOG.md
/Documentation/...
/Config/DefaultDirectiveUtilities.ini

View File

@@ -0,0 +1,60 @@
{
"FileVersion": 3,
"Version": 3,
"VersionName": "2.0",
"FriendlyName": "Directive Utilities",
"Description": "An open-source Unreal Engine plugin that provides runtime and editor utility nodes for developers.",
"Category": "Unreal Directive",
"CreatedBy": "Unreal Directive",
"CreatedByURL": "https://unrealdirective.com",
"DocsURL": "https://udcore.unrealdirective.com/",
"MarketplaceURL": "https://unrealdirective.com",
"SupportURL": "https://github.com/UnrealDirective/DirectiveUtilities/issues",
"CanContainContent": false,
"IsBetaVersion": false,
"IsExperimentalVersion": false,
"Installed": false,
"Modules": [
{
"Name": "DirectiveUtilitiesRuntime",
"Type": "Runtime",
"LoadingPhase": "Default",
"PlatformAllowList": [
"Win64",
"Mac",
"Linux"
]
},
{
"Name": "DirectiveUtilitiesEditor",
"Type": "Editor",
"LoadingPhase": "Default",
"TargetAllowList": [
"Editor"
]
},
{
"Name": "DirectiveUtilitiesTests",
"Type": "DeveloperTool",
"LoadingPhase": "PostEngineInit",
"PlatformAllowList": [
"Win64",
"Mac",
"Linux"
],
"TargetAllowList": [
"Editor"
]
}
],
"Plugins": [
{
"Name": "EditorScriptingUtilities",
"Enabled": true
},
{
"Name": "EnhancedInput",
"Enabled": true
}
]
}

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Unreal Directive
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Binary file not shown.

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128" viewBox="0 0 128 128">
<defs>
<clipPath id="clip-UD_Logo_White">
<rect width="128" height="128"/>
</clipPath>
</defs>
<g id="UD_Logo_White" clip-path="url(#clip-UD_Logo_White)">
<path id="UD_Logo" data-name="UD Logo" d="M-8827.057-11332.424c19.39-5.156,34.011-22.721,34.011-40.855v-50.091a11.545,11.545,0,0,0-3.83-8.447,13.306,13.306,0,0,0-9.221-3.308h-48.677v31.156l-11.379-11.378v-29.784h60.588a20.9,20.9,0,0,1,14.91,6.2,21.007,21.007,0,0,1,6.176,14.938v49.306a41.994,41.994,0,0,1-3.315,16.457,42.1,42.1,0,0,1-9.038,13.431,41.874,41.874,0,0,1-13.4,9.056,41.808,41.808,0,0,1-16.417,3.324Zm-59.661-3.321a42.026,42.026,0,0,1-13.4-9.059,42.077,42.077,0,0,1-9.038-13.434,42.076,42.076,0,0,1-3.317-16.454v-49.3a20.991,20.991,0,0,1,6.178-14.941,20.912,20.912,0,0,1,14.912-6.192h.269c-3,2.026-9.977,7.416-9.977,13.737v56.988c0,18.8,18.541,30.428,35.717,30.428s35.715-11.632,35.715-30.428v-10.632l11.369,11.37a42.052,42.052,0,0,1-3.3,15.427,42.11,42.11,0,0,1-9.038,13.434,42.033,42.033,0,0,1-13.407,9.059,34.379,34.379,0,0,1-8.108,2.487,78.659,78.659,0,0,1-13.229.831C-8876.733-11332.427-8881.637-11333.593-8886.718-11335.745Z" transform="translate(8912.478 11452.777)" fill="#fff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,34 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
using UnrealBuildTool;
public class DirectiveUtilitiesEditor : ModuleRules
{
public DirectiveUtilitiesEditor(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
"CoreUObject",
"Engine",
"EditorSubsystem",
"EditorScriptingUtilities",
"DirectiveUtilitiesRuntime",
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"Slate",
"SlateCore",
"UnrealEd",
"AssetRegistry",
"AssetTools",
}
);
}
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "DirectiveUtilitiesEditor.h"
void FDirectiveUtilitiesEditorModule::StartupModule()
{
}
void FDirectiveUtilitiesEditorModule::ShutdownModule()
{
}
IMPLEMENT_MODULE(FDirectiveUtilitiesEditorModule, DirectiveUtilitiesEditor)

View File

@@ -0,0 +1,290 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilEditorAssetLibrary.h"
#include "Subsystems/EditorAssetSubsystem.h"
#include "Algo/Transform.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "AssetRegistry/ARFilter.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "Misc/AssetRegistryInterface.h"
#include "Modules/ModuleManager.h"
#include "UObject/ObjectRedirector.h"
namespace
{
void WaitForAssetRegistry()
{
if (IAssetRegistry* AssetRegistry = IAssetRegistry::Get())
{
if (AssetRegistry->IsLoadingAssets())
{
AssetRegistry->WaitForCompletion();
}
}
}
}
TArray<FAssetData> UDirectiveUtilEditorAssetLibrary::GetAssetDataListFromDirectory(
const FString& DirectoryPath,
const bool bRecursive)
{
WaitForAssetRegistry();
TArray<FAssetData> AssetDataList;
UEditorAssetSubsystem* EditorAssetSubsystem = GEditor ? GEditor->GetEditorSubsystem<UEditorAssetSubsystem>() : nullptr;
if (!EditorAssetSubsystem)
{
return AssetDataList;
}
const TArray<FString> AssetPaths = EditorAssetSubsystem->ListAssets(DirectoryPath, bRecursive, false);
Algo::Transform(AssetPaths, AssetDataList, [EditorAssetSubsystem](const FString& AssetPath) {
return EditorAssetSubsystem->FindAssetData(AssetPath);
});
AssetDataList.RemoveAll([](const FAssetData& AssetData) {
return !AssetData.IsValid();
});
return AssetDataList;
}
TMap<FDirectiveUtilAssetKey, FDirectiveUtilDuplicateAssetData> UDirectiveUtilEditorAssetLibrary::FindDuplicateAssets(
const TArray<FString>& DirectoryPaths,
const bool bRecursive)
{
TArray<FAssetData> CombinedAssetDataList;
for (const FString& DirectoryPath : DirectoryPaths)
{
const TArray<FAssetData> AssetDataList = GetAssetDataListFromDirectory(DirectoryPath, bRecursive);
CombinedAssetDataList.Append(AssetDataList);
}
TMap<FDirectiveUtilAssetKey, FDirectiveUtilDuplicateAssetData> DuplicateAssetsMap;
for (const FAssetData& AssetData : CombinedAssetDataList)
{
if (!AssetData.IsValid()) { continue; }
const FString AssetName = AssetData.AssetName.ToString();
const FString AssetClass = AssetData.AssetClassPath.ToString();
const FString AssetPath = AssetData.GetSoftObjectPath().ToString();
const FDirectiveUtilAssetKey AssetKey(AssetName, AssetClass);
if (FDirectiveUtilDuplicateAssetData* ExistingData = DuplicateAssetsMap.Find(AssetKey))
{
ExistingData->DuplicateAssetPaths.AddUnique(AssetPath);
}
else
{
FDirectiveUtilDuplicateAssetData DuplicateAssetData;
DuplicateAssetData.AssetName = AssetName;
DuplicateAssetData.AssetClass = AssetClass;
DuplicateAssetData.DuplicateAssetPaths.Add(AssetPath);
DuplicateAssetsMap.Emplace(AssetKey, MoveTemp(DuplicateAssetData));
}
}
TArray<FDirectiveUtilAssetKey> KeysToRemove;
for (const auto& Pair : DuplicateAssetsMap)
{
if (Pair.Value.DuplicateAssetPaths.Num() <= 1)
{
KeysToRemove.Add(Pair.Key);
}
}
for (const FDirectiveUtilAssetKey& Key : KeysToRemove)
{
DuplicateAssetsMap.Remove(Key);
}
return DuplicateAssetsMap;
}
EDirectiveUtilSuccessStatus UDirectiveUtilEditorAssetLibrary::FixUpRedirectorsInPaths(const TArray<FString>& DirectoryPaths, int32& OutRedirectorsProcessed)
{
OutRedirectorsProcessed = 0;
IAssetRegistry* AssetRegistry = IAssetRegistry::Get();
if (!AssetRegistry)
{
return EDirectiveUtilSuccessStatus::Failure;
}
if (AssetRegistry->IsLoadingAssets())
{
AssetRegistry->WaitForCompletion();
}
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
IAssetTools& AssetTools = AssetToolsModule.Get();
if (AssetTools.IsFixupReferencersInProgress())
{
return EDirectiveUtilSuccessStatus::Failure;
}
FARFilter Filter;
Filter.bRecursiveClasses = false;
Filter.ClassPaths.Add(UObjectRedirector::StaticClass()->GetClassPathName());
if (DirectoryPaths.Num() > 0)
{
Filter.bRecursivePaths = true;
for (const FString& DirectoryPath : DirectoryPaths)
{
Filter.PackagePaths.Add(FName(*DirectoryPath));
}
}
TArray<FAssetData> RedirectorAssets;
if (!AssetRegistry->GetAssets(Filter, RedirectorAssets))
{
return EDirectiveUtilSuccessStatus::Failure;
}
TArray<UObjectRedirector*> Redirectors;
Redirectors.Reserve(RedirectorAssets.Num());
for (const FAssetData& RedirectorData : RedirectorAssets)
{
if (UObjectRedirector* Redirector = Cast<UObjectRedirector>(RedirectorData.GetAsset()))
{
Redirectors.Add(Redirector);
}
}
if (Redirectors.Num() == 0)
{
return EDirectiveUtilSuccessStatus::Success;
}
AssetTools.FixupReferencers(Redirectors, false, ERedirectFixupMode::DeleteFixedUpRedirectors);
OutRedirectorsProcessed = Redirectors.Num();
return EDirectiveUtilSuccessStatus::Success;
}
TArray<FAssetData> UDirectiveUtilEditorAssetLibrary::GetAssetsByClass(
UClass* AssetClass,
const FString& PackagePath,
const bool bRecursiveClasses,
const bool bRecursivePaths,
EDirectiveUtilSuccessStatus& OutStatus)
{
WaitForAssetRegistry();
TArray<FAssetData> Result;
OutStatus = EDirectiveUtilSuccessStatus::Failure;
if (!IsValid(AssetClass))
{
return Result;
}
IAssetRegistry* AssetRegistry = IAssetRegistry::Get();
if (!AssetRegistry)
{
return Result;
}
FARFilter Filter;
Filter.ClassPaths.Add(AssetClass->GetClassPathName());
Filter.bRecursiveClasses = bRecursiveClasses;
if (!PackagePath.IsEmpty())
{
Filter.PackagePaths.Add(FName(*PackagePath));
Filter.bRecursivePaths = bRecursivePaths;
}
if (AssetRegistry->GetAssets(Filter, Result))
{
OutStatus = EDirectiveUtilSuccessStatus::Success;
}
return Result;
}
TArray<FString> UDirectiveUtilEditorAssetLibrary::GetAssetDependencies(const FAssetData& Asset, const bool bHardDependenciesOnly, EDirectiveUtilSuccessStatus& OutStatus)
{
using namespace UE::AssetRegistry;
WaitForAssetRegistry();
TArray<FString> Result;
OutStatus = EDirectiveUtilSuccessStatus::Failure;
IAssetRegistry* AssetRegistry = IAssetRegistry::Get();
if (!AssetRegistry || !Asset.IsValid())
{
return Result;
}
TArray<FName> Dependencies;
const FDependencyQuery Query = bHardDependenciesOnly ? FDependencyQuery(EDependencyQuery::Hard) : FDependencyQuery();
if (AssetRegistry->GetDependencies(Asset.PackageName, Dependencies, EDependencyCategory::Package, Query))
{
Result.Reserve(Dependencies.Num());
for (const FName& Dependency : Dependencies)
{
Result.Add(Dependency.ToString());
}
OutStatus = EDirectiveUtilSuccessStatus::Success;
}
return Result;
}
TArray<FString> UDirectiveUtilEditorAssetLibrary::GetAssetReferencers(const FAssetData& Asset, const bool bHardReferencesOnly, EDirectiveUtilSuccessStatus& OutStatus)
{
using namespace UE::AssetRegistry;
WaitForAssetRegistry();
TArray<FString> Result;
OutStatus = EDirectiveUtilSuccessStatus::Failure;
IAssetRegistry* AssetRegistry = IAssetRegistry::Get();
if (!AssetRegistry || !Asset.IsValid())
{
return Result;
}
TArray<FName> Referencers;
const FDependencyQuery Query = bHardReferencesOnly ? FDependencyQuery(EDependencyQuery::Hard) : FDependencyQuery();
if (AssetRegistry->GetReferencers(Asset.PackageName, Referencers, EDependencyCategory::Package, Query))
{
Result.Reserve(Referencers.Num());
for (const FName& Referencer : Referencers)
{
Result.Add(Referencer.ToString());
}
OutStatus = EDirectiveUtilSuccessStatus::Success;
}
return Result;
}
FString UDirectiveUtilEditorAssetLibrary::GetDefaultAssetNameForClass(UClass* AssetClass, EDirectiveUtilSuccessStatus& OutStatus)
{
OutStatus = EDirectiveUtilSuccessStatus::Failure;
if (!IsValid(AssetClass))
{
return FString();
}
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
IAssetTools& AssetTools = AssetToolsModule.Get();
#if ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 8
const TOptional<FString> ResolvedName = AssetTools.GetDefaultAssetNameForClass(AssetClass, nullptr, nullptr);
#else
const TOptional<FString> ResolvedName = AssetTools.GetDefaultAssetNameForClass(AssetClass);
#endif
if (ResolvedName.IsSet() && !ResolvedName.GetValue().IsEmpty())
{
OutStatus = EDirectiveUtilSuccessStatus::Success;
return ResolvedName.GetValue();
}
return FString();
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
class FDirectiveUtilitiesEditorModule : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};

View File

@@ -0,0 +1,102 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "EditorAssetLibrary.h"
#include "Types/DirectiveUtilEditorAssetTypes.h"
#include "Types/DirectiveUtilTypes.h"
#include "DirectiveUtilEditorAssetLibrary.generated.h"
/**
* UDirectiveUtilEditorAssetLibrary
*
* Blueprint helpers for querying and managing editor assets.
*/
UCLASS()
class DIRECTIVEUTILITIESEDITOR_API UDirectiveUtilEditorAssetLibrary : public UEditorAssetLibrary
{
GENERATED_BODY()
public:
/**
* Retrieve a list of asset data for the given directory.
* @param DirectoryPath Directory path of the asset we want the list from. (e.g., /Game/MyFolder or /MyPluginName/MyFolder)
* @param bRecursive The search will be recursive and will look in subfolders. Defaults to true.
* @return TArray<FAssetData> List of asset data.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities | Editor Scripting | Asset")
static TArray<FAssetData> GetAssetDataListFromDirectory(const FString& DirectoryPath, bool bRecursive = true);
/**
* Find and return a list of duplicate assets within the given directories.
* The criteria for duplication is based on the asset name and class.
* @param DirectoryPaths List of directory paths to search for duplicate assets.
* (e.g., /Game/MyFolder or /MyPluginName/MyFolder)
* @param bRecursive The search will be recursive and will look in subfolders. Defaults to true.
* @return TMap<FDirectiveUtilAssetKey, FDirectiveUtilDuplicateAssetData> Mapped list of duplicate assets.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities | Editor Scripting | Asset")
static TMap<FDirectiveUtilAssetKey, FDirectiveUtilDuplicateAssetData> FindDuplicateAssets(
const TArray<FString>& DirectoryPaths,
bool bRecursive = true);
/**
* 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.
* @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).
*/
UFUNCTION(BlueprintCallable, meta = (ExpandEnumAsExecs = "ReturnValue"), Category = "Directive Utilities | Editor Scripting | Asset")
static EDirectiveUtilSuccessStatus FixUpRedirectorsInPaths(const TArray<FString>& DirectoryPaths, int32& OutRedirectorsProcessed);
/**
* Finds all assets of the given class using the Asset Registry (no asset loading).
* @param AssetClass The class to search for.
* @param PackagePath An optional package path to scope the search (e.g. /Game/MyFolder). Empty searches everywhere.
* @param bRecursiveClasses If true, also matches subclasses of AssetClass.
* @param bRecursivePaths If true, also searches subfolders of PackagePath.
* @param OutStatus [out] Success if the registry was queried successfully.
* @return The matching asset data.
*/
UFUNCTION(BlueprintCallable, meta = (ExpandEnumAsExecs = "OutStatus"), Category = "Directive Utilities | Editor Scripting | Asset")
static TArray<FAssetData> GetAssetsByClass(
UClass* AssetClass,
const FString& PackagePath,
bool bRecursiveClasses,
bool bRecursivePaths,
EDirectiveUtilSuccessStatus& OutStatus);
/**
* Returns the package paths of the assets that the given asset depends on, using the Asset Registry dependency graph.
* @param Asset The asset whose dependencies to retrieve.
* @param bHardDependenciesOnly If true, only hard (always-loaded) dependencies are returned.
* @param OutStatus [out] Success if the registry was queried successfully.
* @return The dependency package paths.
*/
UFUNCTION(BlueprintCallable, meta = (ExpandEnumAsExecs = "OutStatus"), Category = "Directive Utilities | Editor Scripting | Asset")
static TArray<FString> GetAssetDependencies(const FAssetData& Asset, bool bHardDependenciesOnly, EDirectiveUtilSuccessStatus& OutStatus);
/**
* Returns the package paths of the assets that reference the given asset, using the Asset Registry dependency graph.
* @param Asset The asset whose referencers to retrieve.
* @param bHardReferencesOnly If true, only hard (always-loaded) referencers are returned.
* @param OutStatus [out] Success if the registry was queried successfully.
* @return The referencer package paths.
*/
UFUNCTION(BlueprintCallable, meta = (ExpandEnumAsExecs = "OutStatus"), Category = "Directive Utilities | Editor Scripting | Asset")
static TArray<FString> GetAssetReferencers(const FAssetData& Asset, bool bHardReferencesOnly, EDirectiveUtilSuccessStatus& OutStatus);
/**
* Returns the default name a new asset of the given class would receive, honoring the project's asset naming
* convention overrides where the engine supports them.
* @note Naming-convention overrides are only consulted on UE 5.8+; on 5.6/5.7 the engine's plain default name is returned.
* @param AssetClass The class to resolve a default asset name for.
* @param OutStatus [out] Success if a non-empty name was resolved.
* @return The default asset name, or an empty string on failure.
*/
UFUNCTION(BlueprintCallable, meta = (ExpandEnumAsExecs = "OutStatus"), Category = "Directive Utilities | Editor Scripting | Asset")
static FString GetDefaultAssetNameForClass(UClass* AssetClass, EDirectiveUtilSuccessStatus& OutStatus);
};

View File

@@ -0,0 +1,695 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/EditorActorSubsystem.h"
#include "Engine/EngineTypes.h"
#include "Types/DirectiveUtilEditorTypes.h"
#include "DirectiveUtilEditorActorSubsystem.generated.h"
class UCapsuleComponent;
class UBoxComponent;
class USphereComponent;
class UStaticMeshActor;
/**
* DirectiveUtilEditorActorSubsystem
*
* Blueprint helpers for querying and filtering actors in the editor world.
*/
UCLASS()
class DIRECTIVEUTILITIESEDITOR_API UDirectiveUtilEditorActorSubsystem : public UEditorActorSubsystem
{
GENERATED_BODY()
public:
//-----------------------------
// Utilities
//-----------------------------
/**
* Focus actors in viewport.
* @param Actors The actors to focus.
* @param bInstant Enable to focus the actors instantly instead of smoothly animating.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor")
static void FocusActorsInViewport(const TArray<AActor*> Actors, bool bInstant = false);
/**
* Get all unique classes used in the level.
* @result Classes The list of classes.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor")
TArray<UClass*> GetAllLevelClasses();
//-----------------------------
// Filters
//-----------------------------
// Output arrays are appended to and deduplicated (existing entries are preserved).
// Passing the same array as input and output filters it in place.
/**
* Returns only the Static Mesh Actors from the provided Actor List.
* @param ActorsToFilter The list of Actors to filter.
* @param OutStaticMeshActors The list of Actors that are Static Mesh Actors.
*/
virtual void FilterStaticMeshActors(TArray<AStaticMeshActor*>& OutStaticMeshActors, TArray<AActor*> ActorsToFilter) const;
/**
* Filters the provided actors based on the provided name.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param ActorName The name to filter by.
* @param Inclusivity Whether to include or exclude actors with the provided name.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterActorsByName(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, const FString& ActorName, EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Filter the provided actors based on the provided class.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param ActorClass The class to filter by.
* @param Inclusivity Whether to include or exclude actors with the provided class.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterActorsByClass(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, TSubclassOf<AActor> ActorClass, EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Filter the provided actors based on the provided tags.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param Tag The tag to filter by.
* @param Inclusivity Whether to include or exclude actors with the provided tags.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterActorsByTag(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, const FName Tag, EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Filter the provided actors based on the provided material name.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param MaterialName The material name to filter by.
* @param MaterialSource The location to check for the material.
* @param Inclusivity Whether to include or exclude actors with the provided material name.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterActorsByMaterialName(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, const FString& MaterialName, EDirectiveUtilSearchLocation MaterialSource, EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Filter the provided actors based on the provided material reference.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param Material The material reference to filter by.
* @param MaterialSource The location to check for the material.
* @param Inclusivity Whether to include or exclude actors with the provided material reference.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterActorsByMaterial(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, const TSoftObjectPtr<UMaterialInterface>& Material, EDirectiveUtilSearchLocation MaterialSource, EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Filter the provided actors based on the provided static mesh name.
* Uses a case-insensitive substring match against the mesh asset name.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param StaticMeshName The static mesh name to filter by.
* @param Inclusivity Whether to include or exclude actors with the provided static mesh name.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterActorsByStaticMeshName(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, const FString& StaticMeshName, EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Filter the provided actors based on the provided static mesh reference.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param StaticMesh The static mesh reference to filter by.
* @param Inclusivity Whether to include or exclude actors with the provided static mesh reference.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterActorsByStaticMesh(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, const TSoftObjectPtr<UStaticMesh>& StaticMesh, EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Filter the provided actors based on the provided vert count range.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param MinVertCount The minimum vert count to filter by.
* @param MaxVertCount The maximum vert count to filter by.
* @param Inclusivity Whether to include or exclude actors with the provided vert count range.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterActorsByVertCount(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, int32 MinVertCount, int32 MaxVertCount, EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Filter the provided actors based on the provided triangle count range.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param MinTriCount The minimum triangle count to filter by.
* @param MaxTriCount The maximum triangle count to filter by.
* @param Inclusivity Whether to include or exclude actors with the provided triangle count range.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterActorsByTriCount(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, int32 MinTriCount, int32 MaxTriCount, EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Filter the provided actors based on the provided actor bounds.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param MinBounds The minimum bounds to filter by.
* @param MaxBounds The maximum bounds to filter by.
* @param Inclusivity Whether to include or exclude actors with the provided bounds.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterActorsByBounds(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, const FVector& MinBounds, const FVector& MaxBounds, EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Filter the provided actors based on the provided static mesh bounds.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param MinBounds The minimum bounds to filter by.
* @param MaxBounds The maximum bounds to filter by.
* @param Inclusivity Whether to include or exclude actors with the provided bounds.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterActorsByStaticMeshBounds(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, const FVector& MinBounds, const FVector& MaxBounds, EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Filter the provided actors based on the provided world location and radius.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param WorldLocation The world location to filter by.
* @param Radius The radius to filter by.
* @param Inclusivity Whether to include or exclude actors with the provided world location and radius.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterActorsByWorldLocation(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, const FVector& WorldLocation, float Radius, EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Filter the provided actors based on the provided LOD (Level of Detail) count.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param MinLODs The minimum LOD count to filter by.
* @param MaxLODs The maximum LOD count to filter by.
* @param Inclusivity Whether to include or exclude actors with the provided LOD count.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterActorsByLODCount(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, int32 MinLODs, int32 MaxLODs, EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Filter the provided actors based on the provided Nanite state.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param bNaniteEnabled Whether to filter by Nanite enabled or disabled.
* @param Inclusivity Whether to include or exclude actors with the provided Nanite state.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterActorsByNaniteState(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, bool bNaniteEnabled, EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Filter the provided actors based on the provided Lightmap Resolution.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param MinLightmapResolution The minimum lightmap resolution to filter by.
* @param MaxLightmapResolution The maximum lightmap resolution to filter by.
* @param SearchLocation The location to search from (Actor Override and/or Static Mesh).
* @param Inclusivity Whether to include or exclude actors with the provided lightmap resolution.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterActorsByLightmapResolution(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, int32 MinLightmapResolution, int32 MaxLightmapResolution, EDirectiveUtilSearchLocation SearchLocation, EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Filter the provided actors based on the provided mobility.
* Tests the root component's mobility, matching the actor mobility shown in the editor UI.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param Mobility The mobility to filter by.
* @param Inclusivity Whether to include or exclude actors with the provided mobility.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterActorsByMobility(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, EComponentMobility::Type Mobility, EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Filter the provided actors based on the provided collision channel.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param CollisionChannel The collision type to filter by.
* @param Inclusivity Whether to include or exclude actors with the provided collision type.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterActorsByCollisionChannel(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, ECollisionChannel CollisionChannel, EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Filter the provided actors based on the provided collision response.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param CollisionChannel The collision channel to filter by.
* @param CollisionResponse The collision response to filter by.
* @param Inclusivity Whether to include or exclude actors with the provided collision response.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterActorsByCollisionResponse(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, ECollisionChannel CollisionChannel, ECollisionResponse CollisionResponse, EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Filter the provided actors based on the provided collision-enabled state.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param CollisionEnabled The collision state to filter by.
* @param Inclusivity Whether to include or exclude actors with the provided collision-enabled state.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterActorsByCollisionEnabled(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, ECollisionEnabled::Type CollisionEnabled, EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Filter the provided actors based on the provided collision profile.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param CollisionProfile The collision profile to filter by.
* @param Inclusivity Whether to include or exclude actors with the provided collision profile.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterActorsByCollisionProfile(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, FName CollisionProfile, EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Filter the provided actors based on the provided Texture Name.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param TextureName The texture name to filter by.
* @param Source Chose between searching through material overrides or the base material.
* @param Inclusivity Whether to include or exclude actors with the provided texture name.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterActorsByTextureName(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, FString TextureName, EDirectiveUtilSearchLocation Source, EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Filter the provided actors based on the provided Texture Reference.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param TextureReference The texture reference to filter by.
* @param Source Chose between searching through material overrides or the base material.
* @param Inclusivity Whether to include or exclude actors with the provided texture reference.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterActorsByTexture(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, TSoftObjectPtr<UTexture2D> TextureReference, EDirectiveUtilSearchLocation Source, EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Filters the provided actors based on if the actor is empty or not.
* An actor is empty when it has no components, or its only component is a
* plain scene component with no child components.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param Inclusivity Whether to include or exclude empty actors.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterEmptyActors(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Filter the provided actors based on missing materials.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param Location The location to search for missing materials.
* @param Inclusivity Whether to include or exclude actors with missing materials.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterActorsByMissingMaterials(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, EDirectiveUtilSearchLocation Location, EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Filter the provided actors based on missing Static Meshes.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param Inclusivity Whether to include or exclude actors with missing Static Meshes.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterActorsByMissingStaticMeshes(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Filter the provided actors based on missing textures.
* @param Actors The list of actors to filter.
* @param FilteredActors The list of actors that have been filtered.
* @param Location The location to search for missing textures.
* @param Inclusivity Whether to include or exclude actors with missing textures.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Filters|Actor")
static void FilterActorsByMissingTextures(const TArray<AActor*>& Actors, TArray<AActor*>& FilteredActors, EDirectiveUtilSearchLocation Location, EDirectiveUtilInclusivity Inclusivity = Include);
//-----------------------------
// Bounds Calculation
//-----------------------------
/**
* Check if an actor is within the bounds of a box.
* @param Actor The actor to check.
* @param BoxComponent The box component to check.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities")
static bool IsActorWithinBoxBounds(AActor* Actor, UBoxComponent* BoxComponent);
/**
* Check if an actor is within the bounds of a Sphere.
* @param Actor The actor to check.
* @param SphereComponent The sphere component to check.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities")
static bool IsActorWithinSphereBounds(AActor* Actor, USphereComponent* SphereComponent);
/**
* Check if an actor is within the bounds of a capsule.
* @param Actor The actor to check.
* @param CapsuleComponent The capsule component to check.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities")
static bool IsActorWithinCapsuleBounds(AActor* Actor, UCapsuleComponent* CapsuleComponent);
//-----------------------------
// Getters
//-----------------------------
/**
* Returns a list of actors based on the provided class and options.
* Get actors within the current level by their class.
* @param FoundActors The list of actors that were found.
* @param ActorClass The class of the actors to select.
* @param SelectionMethod The selection method to use.
* @param Inclusivity Should the search be inclusive or exclusive?
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(AdvancedDisplay=2))
void GetActorsByClass(
TArray<AActor*>& FoundActors,
TSubclassOf<AActor> ActorClass,
EDirectiveUtilSelectionMethod SelectionMethod = World,
EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Returns a list of actors based on the provided asset name and options.
* @param FoundActors The list of actors that were found.
* @param ActorName The name of the actors to select.
* @param SelectionMethod The selection method to use.
* @param Inclusivity Should the search be inclusive or exclusive?
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(AdvancedDisplay=2))
void GetActorsByName(
TArray<AActor*>& FoundActors,
FString ActorName,
EDirectiveUtilSelectionMethod SelectionMethod = World,
EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Returns a list of actors based on the provided material reference and options.
* Note: This will only return actors that have a static mesh component.
* @param Material The reference of the material to search by.
* @param FoundActors The list of actors that were found.
* @param MaterialSource The source of the material to search by.
* @param SelectionMethod The selection method to use.
* @param Inclusivity Should the search be inclusive or exclusive?
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(AdvancedDisplay=4))
void GetActorsByMaterial(
TArray<AActor*>& FoundActors,
const UMaterialInterface* Material,
EDirectiveUtilSearchLocation MaterialSource = BaseAndOverride,
EDirectiveUtilSelectionMethod SelectionMethod = World,
EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Returns a list of actors based on the provided material reference and options.
* Note: This will only return actors that have a static mesh component.
* @param Material The reference of the material to search by.
* @param FoundActors The list of actors that were found.
* @param MaterialSource The source of the material to search by.
* @param SelectionMethod The selection method to use.
* @param Inclusivity Should the search be inclusive or exclusive?
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(AdvancedDisplay=4))
void GetActorsByMaterialSoftReference(
TArray<AActor*>& FoundActors,
const TSoftObjectPtr<UMaterialInterface> Material,
EDirectiveUtilSearchLocation MaterialSource = BaseAndOverride,
EDirectiveUtilSelectionMethod SelectionMethod = World,
EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Returns a list of actors based on the provided material name and options.
* Note: This will only return actors that have a static mesh component.
* @param MaterialName The name of the material to search by.
* @param FoundActors The list of actors that were found.
* @param MaterialSource The source of the material to search by.
* @param SelectionMethod The selection method to use.
* @param Inclusivity Should the search be inclusive or exclusive?
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(AdvancedDisplay=4))
void GetActorsByMaterialName(
TArray<AActor*>& FoundActors,
FString MaterialName,
EDirectiveUtilSearchLocation MaterialSource = BaseAndOverride,
EDirectiveUtilSelectionMethod SelectionMethod = World,
EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Returns a list of actors based on the provided vert count and options.
* Note: This will only return actors that have a Static Mesh Component.
* @param FoundActors The list of actors that were found.
* @param From The minimum number of vertices to search for.
* @param To The maximum number of vertices to search for.
* @param SelectionMethod The selection method to use.
* @param Inclusivity Should the search be inclusive or exclusive?
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(AdvancedDisplay=3))
void GetActorsByVertexCount(
TArray<AActor*>& FoundActors,
int32 From,
int32 To,
EDirectiveUtilSelectionMethod SelectionMethod = World,
EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Returns a list of actors based on the provided triangle count and options.
* Note: This will only return actors that have a Static Mesh Component.
* @param FoundActors The list of actors that were found.
* @param From The minimum number of vertices to search for.
* @param To The maximum number of vertices to search for.
* @param SelectionMethod The selection method to use.
* @param Inclusivity Should the search be inclusive or exclusive?
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(AdvancedDisplay=3))
void GetActorsByTriCount(
TArray<AActor*>& FoundActors,
int32 From,
int32 To,
EDirectiveUtilSelectionMethod SelectionMethod = World,
EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Returns a list of actors based on the provided bounding box and options.
* Matches actors whose world-space bounding box lies entirely within the box defined by Min/Max (inclusive).
* @param FoundActors The list of actors that were found.
* @param Min The minimum point of the bounding box.
* @param Max The maximum point of the bounding box.
* @param SelectionMethod The selection method to use.
* @param Inclusivity Should the search be inclusive or exclusive?
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(AdvancedDisplay=3))
void GetActorsByBoundingBox(
TArray<AActor*>& FoundActors,
FVector Min,
FVector Max,
EDirectiveUtilSelectionMethod SelectionMethod = World,
EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Returns a list of actors based on the provided mesh size and options.
* @param FoundActors The list of actors that were found.
* @param From The minimum size of the mesh to search for.
* @param To The maximum size of the mesh to search for.
* @param SelectionMethod The selection method to use.
* @param Inclusivity Should the search be inclusive or exclusive?
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(AdvancedDisplay=3))
void GetActorsByMeshSize(
TArray<AActor*>& FoundActors,
float From,
float To,
EDirectiveUtilSelectionMethod SelectionMethod = World,
EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Returns a list of actors based on the provided world location, radius, and options.
* @param FoundActors The list of actors that were found.
* @param WorldLocation The world location to search by.
* @param Radius The radius around the provided world location to search by.
* @param SelectionMethod The selection method to use.
* @param Inclusivity Should the search be inclusive or exclusive?
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(AdvancedDisplay=3))
void GetActorsByWorldLocation(
TArray<AActor*>& FoundActors,
FVector WorldLocation,
float Radius = 1000.f,
EDirectiveUtilSelectionMethod SelectionMethod = World,
EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Returns a list of actors based on the provided LOD count and options.
* @param FoundActors The list of actors that were found.
* @param From The minimum number of LODs to search for.
* @param To The maximum number of LODs to search for.
* @param SelectionMethod The selection method to use.
* @param Inclusivity Should the search be inclusive or exclusive?
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(AdvancedDisplay=3))
void GetActorsByLODCount(
TArray<AActor*>& FoundActors,
int32 From = 0,
int32 To = 7,
EDirectiveUtilSelectionMethod SelectionMethod = World,
EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Returns a list of actors based on if they have Nanite enabled or not.
* Note: This will only return actors that have a Static Mesh Component.
* @param FoundActors The list of actors that were found.
* @param bNaniteEnabled Enable to find Actors with Static Mesh Components that have Nanite enabled. Disable to find Actors with Static Mesh Components that do not have Nanite enabled.
* @param SelectionMethod The selection method to use.
* @param Inclusivity Should the search be inclusive or exclusive?
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(AdvancedDisplay=2))
void GetActorsByNaniteEnabled(
TArray<AActor*>& FoundActors,
bool bNaniteEnabled,
EDirectiveUtilSelectionMethod SelectionMethod = World,
EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Returns a list of actors based on the provided Lightmap Resolution and options.
* @param FoundActors The list of actors that were found.
* @param From The minimum lightmap resolution to search for.
* @param To The maximum lightmap resolution to search for.
* @param SelectionMethod The selection method to use.
* @param Inclusivity Should the search be inclusive or exclusive?
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(AdvancedDisplay=3))
void GetActorsByLightmapResolution(
TArray<AActor*>& FoundActors,
int32 From = 4,
int32 To = 4096,
EDirectiveUtilSelectionMethod SelectionMethod = World,
EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Returns a list of actors based on the provided mobility and options.
* Tests the root component's mobility, matching the actor mobility shown in the editor UI.
* @param FoundActors The list of actors that were found.
* @param SelectionMethod The selection method to use.
* @param Inclusivity Should the search be inclusive or exclusive?
* @param Mobility The mobility to search for.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(AdvancedDisplay=2))
void GetActorsByMobility(
TArray<AActor*>& FoundActors,
EComponentMobility::Type Mobility,
EDirectiveUtilSelectionMethod SelectionMethod = World,
EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Returns a list of actors based on the provided Static Mesh reference and options.
* @param FoundActors The list of actors that were found.
* @param StaticMesh The Static Mesh to search for.
* @param SelectionMethod The selection method to use.
* @param Inclusivity Should the search be inclusive or exclusive?
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(AdvancedDisplay=2))
void GetActorsByStaticMesh(
TArray<AActor*>& FoundActors,
UStaticMesh* StaticMesh,
EDirectiveUtilSelectionMethod SelectionMethod = World,
EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Returns a list of actors based on the provided Static Mesh soft reference and options.
* @param FoundActors The list of actors that were found.
* @param StaticMesh The Static Mesh Soft Reference to search for.
* @param SelectionMethod The selection method to use.
* @param Inclusivity Should the search be inclusive or exclusive?
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(AdvancedDisplay=2))
void GetActorsByStaticMeshSoftReference(
TArray<AActor*>& FoundActors,
TSoftObjectPtr<UStaticMesh> StaticMesh,
EDirectiveUtilSelectionMethod SelectionMethod = World,
EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Returns a list of actors based on the provided Static Mesh name and options.
* Uses a case-insensitive substring match against the mesh asset name.
* @param FoundActors The list of actors that were found.
* @param StaticMeshName The Static Mesh name to search for.
* @param SelectionMethod The selection method to use.
* @param Inclusivity Should the search be inclusive or exclusive?
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(AdvancedDisplay=2))
void GetActorsByStaticMeshName(
TArray<AActor*>& FoundActors,
FString StaticMeshName,
EDirectiveUtilSelectionMethod SelectionMethod = World,
EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Returns a list of actors based on the provided texture reference and options.
* @param FoundActors The list of actors that were found.
* @param Texture The texture to search for.
* @param SelectionMethod The selection method to use.
* @param Inclusivity Should the search be inclusive or exclusive?
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(AdvancedDisplay=2))
void GetActorsByTexture(
TArray<AActor*>& FoundActors,
UTexture2D* Texture,
EDirectiveUtilSelectionMethod SelectionMethod = World,
EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Returns a list of actors based on the provided texture soft reference and options.
* @param FoundActors The list of actors that were found.
* @param Texture The texture soft reference to search for.
* @param SelectionMethod The selection method to use.
* @param Inclusivity Should the search be inclusive or exclusive?
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(AdvancedDisplay=2))
void GetActorsByTextureSoftReference(
TArray<AActor*>& FoundActors,
TSoftObjectPtr<UTexture2D> Texture,
EDirectiveUtilSelectionMethod SelectionMethod = World,
EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Returns a list of actors based on the provided texture name and options.
* @param FoundActors The list of actors that were found.
* @param TextureName The texture name to search for.
* @param SelectionMethod The selection method to use.
* @param Inclusivity Should the search be inclusive or exclusive?
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(AdvancedDisplay=2))
void GetActorsByTextureName(
TArray<AActor*>& FoundActors,
FString TextureName,
EDirectiveUtilSelectionMethod SelectionMethod = World,
EDirectiveUtilInclusivity Inclusivity = Include);
/**
* Returns a list of invalid actors.
* @param FoundActors The list of actors that were found.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(AdvancedDisplay=1))
void GetInvalidActors(TArray<AActor*>& FoundActors);
//-----------------------------
// Static Mesh
//-----------------------------
/**
* Pushes the overriden materials on the provided Static Mesh Component to the source Static Mesh.
* @param StaticMeshComponent The Static Mesh Component to push the materials from.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Static Mesh")
static void PushOverrideMaterialsToSource(UStaticMeshComponent* StaticMeshComponent);
};

View File

@@ -0,0 +1,55 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "DirectiveUtilEditorAssetTypes.generated.h"
USTRUCT(BlueprintType)
struct FDirectiveUtilAssetKey
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset")
FString AssetName;
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset")
FString AssetClass;
FDirectiveUtilAssetKey(const FString& InAssetName, const FString& InAssetClass)
: AssetName(InAssetName), AssetClass(InAssetClass) {}
bool operator==(const FDirectiveUtilAssetKey& Other) const
{
return AssetName == Other.AssetName && AssetClass == Other.AssetClass;
}
friend uint32 GetTypeHash(const FDirectiveUtilAssetKey& Key)
{
return HashCombine(GetTypeHash(Key.AssetName), GetTypeHash(Key.AssetClass));
}
FDirectiveUtilAssetKey(): AssetName(FString()), AssetClass(FString()) {}
};
USTRUCT(BlueprintType)
struct FDirectiveUtilDuplicateAssetData
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset")
FString AssetName;
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset")
FString AssetClass;
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset")
TArray<FString> DuplicateAssetPaths;
FDirectiveUtilDuplicateAssetData()
{
AssetName = FString();
AssetClass = FString();
DuplicateAssetPaths = TArray<FString>();
}
};

View File

@@ -0,0 +1,43 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
/**
* EDirectiveUtilSelectionMethod
*
* The method type used to select actors within the world.
*/
UENUM(BlueprintType, Category = "Directive Utilities")
enum EDirectiveUtilSelectionMethod : uint8
{
World UMETA(DisplayName = "World", Tooltip="Select based on the actors within the world."),
Selection UMETA(DisplayName = "Selection", Tooltip="Select based on the actors within the current selection."),
};
/**
* EDirectiveUtilInclusivity
*
* The inclusivity type used to select actors within the world.
*/
UENUM(BlueprintType, Category = "Directive Utilities")
enum EDirectiveUtilInclusivity : uint8
{
Include UMETA(DisplayName = "Include", Tooltip="Include items based on the provided criteria."),
Exclude UMETA(DisplayName = "Exclude", Tooltip="Exclude items based on the provided criteria."),
};
/**
* EDirectiveUtilSearchLocation
*
* The object source to use.
*/
UENUM(BlueprintType, Category = "Directive Utilities")
enum EDirectiveUtilSearchLocation : uint8
{
BaseAndOverride UMETA(DisplayName = "Base & Override",
Tooltip="With search the base object along with actor overrides."),
BaseOnly UMETA(DisplayName = "Base Only", Tooltip="Will only search the base object."),
OverrideOnly UMETA(DisplayName = "Override Only", Tooltip="Will only search actor overrides."),
};

View File

@@ -0,0 +1,42 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
using UnrealBuildTool;
public class DirectiveUtilitiesRuntime : ModuleRules
{
public DirectiveUtilitiesRuntime(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
"GameplayTags",
"NetCore",
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"CoreUObject",
"Engine",
"Slate",
"SlateCore",
"AIModule",
"EnhancedInput",
"ApplicationCore",
}
);
DynamicallyLoadedModuleNames.AddRange(
new string[]
{
}
);
}
}

View File

@@ -0,0 +1,4 @@
#include "DirectiveUtilLogChannels.h"
DEFINE_LOG_CATEGORY(LogDirectiveUtil);
DEFINE_LOG_CATEGORY(LogDirectiveUtilEditor);

View File

@@ -0,0 +1,13 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "DirectiveUtilitiesRuntime.h"
void FDirectiveUtilitiesRuntimeModule::StartupModule()
{
}
void FDirectiveUtilitiesRuntimeModule::ShutdownModule()
{
}
IMPLEMENT_MODULE(FDirectiveUtilitiesRuntimeModule, DirectiveUtilitiesRuntime)

View File

@@ -0,0 +1,565 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilArrayFunctionLibrary.h"
int32 UDirectiveUtilArrayFunctionLibrary::Array_NextIndex(const TArray<int32>& TargetArray, const int32 Index, const bool bLoop)
{
checkNoEntry();
return 0;
}
int32 UDirectiveUtilArrayFunctionLibrary::Array_PreviousIndex(
const TArray<int32>& TargetArray,
const int32 Index,
const bool bLoop)
{
checkNoEntry();
return 0;
}
int32 UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(
const void* TargetArray,
const FArrayProperty* ArrayProperty,
const int32 Index,
const bool bLoop)
{
if (!TargetArray || !ArrayProperty)
{
return INDEX_NONE;
}
const FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
const int32 NextIndex = Index + 1;
if(ArrayHelper.Num() == 0)
{
return INDEX_NONE;
}
if(NextIndex < 0)
{
return 0;
}
if (NextIndex <= ArrayHelper.Num() - 1)
{
return NextIndex;
}
if (bLoop)
{
return 0;
}
return ArrayHelper.Num() - 1;
}
void UDirectiveUtilArrayFunctionLibrary::Array_RemoveDuplicates(const TArray<int32>& TargetArray)
{
checkNoEntry();
}
void UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveDuplicates(
void* TargetArray,
const FArrayProperty* ArrayProperty)
{
if (!TargetArray || !ArrayProperty)
{
return;
}
FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
const FProperty* InnerProp = ArrayProperty->Inner;
for (int32 OuterIndex = ArrayHelper.Num() - 1; OuterIndex > 0; --OuterIndex)
{
for (int32 InnerIndex = 0; InnerIndex < OuterIndex; ++InnerIndex)
{
if (InnerProp->Identical(ArrayHelper.GetElementPtr(OuterIndex), ArrayHelper.GetElementPtr(InnerIndex)))
{
ArrayHelper.RemoveValues(OuterIndex);
break;
}
}
}
}
int32 UDirectiveUtilArrayFunctionLibrary::GenericArray_PreviousIndex(
const void* TargetArray,
const FArrayProperty* ArrayProperty,
const int32 Index,
const bool bLoop)
{
if (!TargetArray || !ArrayProperty)
{
return INDEX_NONE;
}
const FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
const int32 PreviousIndex = Index - 1;
if(ArrayHelper.Num() == 0)
{
return INDEX_NONE;
}
if(PreviousIndex > ArrayHelper.Num() - 1)
{
return ArrayHelper.Num() - 1;
}
if (PreviousIndex >= 0)
{
return PreviousIndex;
}
if (bLoop)
{
return ArrayHelper.Num() - 1;
}
return 0;
}
bool UDirectiveUtilArrayFunctionLibrary::Array_GetValidFirstItemCopy(const TArray<int32>& TargetArray, int32& OutItem)
{
checkNoEntry();
return false;
}
bool UDirectiveUtilArrayFunctionLibrary::Array_GetValidLastItemCopy(const TArray<int32>& TargetArray, int32& OutItem)
{
checkNoEntry();
return false;
}
bool UDirectiveUtilArrayFunctionLibrary::Array_GetValidItemFromIndexCopy(const TArray<int32>& TargetArray, const int32 Index, int32& OutItem)
{
checkNoEntry();
return false;
}
bool UDirectiveUtilArrayFunctionLibrary::Array_GetRandomItem(const TArray<int32>& TargetArray, int32& OutItem, int32& OutIndex)
{
checkNoEntry();
return false;
}
void UDirectiveUtilArrayFunctionLibrary::Array_LastValue(const TArray<int32>& TargetArray, int32& OutItem)
{
checkNoEntry();
}
bool UDirectiveUtilArrayFunctionLibrary::Array_Pop(const TArray<int32>& TargetArray, int32& OutItem)
{
checkNoEntry();
return false;
}
bool UDirectiveUtilArrayFunctionLibrary::Array_PopFirst(const TArray<int32>& TargetArray, int32& OutItem)
{
checkNoEntry();
return false;
}
bool UDirectiveUtilArrayFunctionLibrary::Array_RemoveAtSwap(const TArray<int32>& TargetArray, const int32 Index)
{
checkNoEntry();
return false;
}
bool UDirectiveUtilArrayFunctionLibrary::GenericArray_GetItemAtIndex(
const void* TargetArray,
const FArrayProperty* ArrayProperty,
const int32 Index,
void* OutItemPtr)
{
if (!TargetArray || !ArrayProperty)
{
return false;
}
FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
const FProperty* InnerProp = ArrayProperty->Inner;
if (!ArrayHelper.IsValidIndex(Index))
{
if (OutItemPtr)
{
InnerProp->ClearValue(OutItemPtr);
}
return false;
}
if (OutItemPtr)
{
InnerProp->CopyCompleteValueFromScriptVM(OutItemPtr, ArrayHelper.GetRawPtr(Index));
}
return true;
}
bool UDirectiveUtilArrayFunctionLibrary::GenericArray_GetFirstItem(
const void* TargetArray,
const FArrayProperty* ArrayProperty,
void* OutItemPtr)
{
return GenericArray_GetItemAtIndex(TargetArray, ArrayProperty, 0, OutItemPtr);
}
bool UDirectiveUtilArrayFunctionLibrary::GenericArray_GetLastItem(
const void* TargetArray,
const FArrayProperty* ArrayProperty,
void* OutItemPtr)
{
if (!TargetArray || !ArrayProperty)
{
return false;
}
const FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
return GenericArray_GetItemAtIndex(TargetArray, ArrayProperty, ArrayHelper.Num() - 1, OutItemPtr);
}
bool UDirectiveUtilArrayFunctionLibrary::GenericArray_GetRandomItem(
const void* TargetArray,
const FArrayProperty* ArrayProperty,
void* OutItemPtr,
int32* OutIndex)
{
if (OutIndex)
{
*OutIndex = INDEX_NONE;
}
if (!TargetArray || !ArrayProperty)
{
return false;
}
const FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
const int32 Num = ArrayHelper.Num();
if (Num <= 0)
{
if (OutItemPtr)
{
ArrayProperty->Inner->ClearValue(OutItemPtr);
}
return false;
}
const int32 Index = FMath::RandRange(0, Num - 1);
if (OutIndex)
{
*OutIndex = Index;
}
return GenericArray_GetItemAtIndex(TargetArray, ArrayProperty, Index, OutItemPtr);
}
bool UDirectiveUtilArrayFunctionLibrary::GenericArray_Pop(
void* TargetArray,
const FArrayProperty* ArrayProperty,
void* OutItemPtr)
{
if (!TargetArray || !ArrayProperty)
{
return false;
}
FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
const FProperty* InnerProp = ArrayProperty->Inner;
const int32 LastIndex = ArrayHelper.Num() - 1;
if (LastIndex < 0)
{
if (OutItemPtr)
{
InnerProp->ClearValue(OutItemPtr);
}
return false;
}
if (OutItemPtr)
{
InnerProp->CopyCompleteValueFromScriptVM(OutItemPtr, ArrayHelper.GetRawPtr(LastIndex));
}
ArrayHelper.RemoveValues(LastIndex, 1);
return true;
}
bool UDirectiveUtilArrayFunctionLibrary::GenericArray_PopFirst(
void* TargetArray,
const FArrayProperty* ArrayProperty,
void* OutItemPtr)
{
if (!TargetArray || !ArrayProperty)
{
return false;
}
FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
const FProperty* InnerProp = ArrayProperty->Inner;
if (ArrayHelper.Num() <= 0)
{
if (OutItemPtr)
{
InnerProp->ClearValue(OutItemPtr);
}
return false;
}
if (OutItemPtr)
{
InnerProp->CopyCompleteValueFromScriptVM(OutItemPtr, ArrayHelper.GetRawPtr(0));
}
ArrayHelper.RemoveValues(0, 1);
return true;
}
bool UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtSwap(
void* TargetArray,
const FArrayProperty* ArrayProperty,
const int32 Index)
{
if (!TargetArray || !ArrayProperty)
{
return false;
}
FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
if (!ArrayHelper.IsValidIndex(Index))
{
return false;
}
const int32 LastIndex = ArrayHelper.Num() - 1;
if (Index != LastIndex)
{
ArrayHelper.SwapValues(Index, LastIndex);
}
ArrayHelper.RemoveValues(LastIndex, 1);
return true;
}
void UDirectiveUtilArrayFunctionLibrary::Array_Slice(const TArray<int32>& TargetArray, const int32 StartIndex, const int32 Count, TArray<int32>& OutArray)
{
checkNoEntry();
}
void UDirectiveUtilArrayFunctionLibrary::Array_Rotate(const TArray<int32>& TargetArray, const int32 Shift)
{
checkNoEntry();
}
void UDirectiveUtilArrayFunctionLibrary::Array_GetDistinct(const TArray<int32>& TargetArray, TArray<int32>& OutArray)
{
checkNoEntry();
}
int32 UDirectiveUtilArrayFunctionLibrary::Array_CountOccurrences(const TArray<int32>& TargetArray, const int32& ItemToCount)
{
checkNoEntry();
return 0;
}
bool UDirectiveUtilArrayFunctionLibrary::Array_GetMostCommon(const TArray<int32>& TargetArray, int32& OutItem, int32& OutCount)
{
checkNoEntry();
return false;
}
void UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(
const void* TargetArray,
const FArrayProperty* TargetArrayProperty,
const int32 StartIndex,
const int32 Count,
void* OutArray,
const FArrayProperty* OutArrayProperty)
{
if (!TargetArray || !OutArray || !TargetArrayProperty || !OutArrayProperty)
{
return;
}
FScriptArrayHelper SourceHelper(TargetArrayProperty, TargetArray);
FScriptArrayHelper OutHelper(OutArrayProperty, OutArray);
OutHelper.EmptyValues();
const int32 Num = SourceHelper.Num();
if (Num == 0 || Count <= 0)
{
return;
}
const FProperty* InnerProp = TargetArrayProperty->Inner;
const int32 Start = FMath::Clamp(StartIndex, 0, Num);
const int32 NumToCopy = FMath::Min(Count, Num - Start);
for (int32 Offset = 0; Offset < NumToCopy; ++Offset)
{
const int32 OutIndex = OutHelper.AddValue();
InnerProp->CopySingleValueToScriptVM(OutHelper.GetRawPtr(OutIndex), SourceHelper.GetRawPtr(Start + Offset));
}
}
void UDirectiveUtilArrayFunctionLibrary::GenericArray_Rotate(
void* TargetArray,
const FArrayProperty* ArrayProperty,
const int32 Shift)
{
if (!TargetArray || !ArrayProperty)
{
return;
}
FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
const int32 Num = ArrayHelper.Num();
if (Num <= 1)
{
return;
}
int32 Normalized = Shift % Num;
if (Normalized < 0)
{
Normalized += Num;
}
if (Normalized == 0)
{
return;
}
auto ReverseRange = [&ArrayHelper](int32 Low, int32 High)
{
while (Low < High)
{
ArrayHelper.SwapValues(Low, High);
++Low;
--High;
}
};
ReverseRange(0, Num - 1);
ReverseRange(0, Normalized - 1);
ReverseRange(Normalized, Num - 1);
}
void UDirectiveUtilArrayFunctionLibrary::GenericArray_GetDistinct(
const void* TargetArray,
const FArrayProperty* TargetArrayProperty,
void* OutArray,
const FArrayProperty* OutArrayProperty)
{
if (!TargetArray || !OutArray || !TargetArrayProperty || !OutArrayProperty)
{
return;
}
FScriptArrayHelper SourceHelper(TargetArrayProperty, TargetArray);
FScriptArrayHelper OutHelper(OutArrayProperty, OutArray);
OutHelper.EmptyValues();
const FProperty* InnerProp = TargetArrayProperty->Inner;
const int32 Num = SourceHelper.Num();
for (int32 SourceIndex = 0; SourceIndex < Num; ++SourceIndex)
{
const uint8* SourceElement = SourceHelper.GetRawPtr(SourceIndex);
bool bIsDuplicate = false;
for (int32 ExistingIndex = 0; ExistingIndex < OutHelper.Num(); ++ExistingIndex)
{
if (InnerProp->Identical(SourceElement, OutHelper.GetRawPtr(ExistingIndex)))
{
bIsDuplicate = true;
break;
}
}
if (!bIsDuplicate)
{
const int32 OutIndex = OutHelper.AddValue();
InnerProp->CopySingleValueToScriptVM(OutHelper.GetRawPtr(OutIndex), SourceElement);
}
}
}
int32 UDirectiveUtilArrayFunctionLibrary::GenericArray_CountOccurrences(
const void* TargetArray,
const FArrayProperty* ArrayProperty,
const void* ItemToCount)
{
if (!TargetArray || !ArrayProperty || !ItemToCount)
{
return 0;
}
FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
const FProperty* InnerProp = ArrayProperty->Inner;
int32 Count = 0;
for (int32 Index = 0; Index < ArrayHelper.Num(); ++Index)
{
if (InnerProp->Identical(ArrayHelper.GetRawPtr(Index), ItemToCount))
{
++Count;
}
}
return Count;
}
bool UDirectiveUtilArrayFunctionLibrary::GenericArray_GetMostCommon(
const void* TargetArray,
const FArrayProperty* ArrayProperty,
void* OutItemPtr,
int32* OutCount)
{
if (OutCount)
{
*OutCount = 0;
}
if (!TargetArray || !ArrayProperty)
{
return false;
}
FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
const FProperty* InnerProp = ArrayProperty->Inner;
const int32 Num = ArrayHelper.Num();
if (Num == 0)
{
if (OutItemPtr)
{
InnerProp->ClearValue(OutItemPtr);
}
return false;
}
int32 BestIndex = 0;
int32 BestCount = 0;
for (int32 Index = 0; Index < Num; ++Index)
{
int32 CurrentCount = 0;
for (int32 CompareIndex = 0; CompareIndex < Num; ++CompareIndex)
{
if (InnerProp->Identical(ArrayHelper.GetRawPtr(Index), ArrayHelper.GetRawPtr(CompareIndex)))
{
++CurrentCount;
}
}
if (CurrentCount > BestCount)
{
BestCount = CurrentCount;
BestIndex = Index;
}
}
if (OutItemPtr)
{
InnerProp->CopyCompleteValueFromScriptVM(OutItemPtr, ArrayHelper.GetRawPtr(BestIndex));
}
if (OutCount)
{
*OutCount = BestCount;
}
return true;
}

View File

@@ -0,0 +1,87 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilFunctionLibrary.h"
#include "HAL/PlatformApplicationMisc.h"
#include "Misc/CommandLine.h"
#include "Misc/ConfigCacheIni.h"
void UDirectiveUtilFunctionLibrary::GetChildClasses(const UClass* BaseClass, const bool bRecursive, TArray<UClass*>& DerivedClasses)
{
GetDerivedClasses(BaseClass, DerivedClasses, bRecursive);
}
void UDirectiveUtilFunctionLibrary::CopyTextToClipboard(const FText& Text)
{
const FString ClipboardText = Text.ToString();
FPlatformApplicationMisc::ClipboardCopy(*ClipboardText);
}
void UDirectiveUtilFunctionLibrary::CopyStringToClipboard(const FString& String)
{
FPlatformApplicationMisc::ClipboardCopy(*String);
}
FText UDirectiveUtilFunctionLibrary::GetTextFromClipboard()
{
FString ClipboardText;
FPlatformApplicationMisc::ClipboardPaste(ClipboardText);
return FText::FromString(ClipboardText);
}
FString UDirectiveUtilFunctionLibrary::GetStringFromClipboard()
{
FString ClipboardText;
FPlatformApplicationMisc::ClipboardPaste(ClipboardText);
return ClipboardText;
}
void UDirectiveUtilFunctionLibrary::ClearClipboard()
{
FPlatformApplicationMisc::ClipboardCopy(TEXT(""));
}
FString UDirectiveUtilFunctionLibrary::GetProjectVersion()
{
FString ProjectVersion;
GConfig->GetString(
TEXT("/Script/EngineSettings.GeneralProjectSettings"),
TEXT("ProjectVersion"),
ProjectVersion,
GGameIni);
return ProjectVersion;
}
bool UDirectiveUtilFunctionLibrary::IsRunningInEditor()
{
return GIsEditor;
}
bool UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(const FString& Switch)
{
return HasCommandLineSwitch(FCommandLine::Get(), Switch);
}
bool UDirectiveUtilFunctionLibrary::GetCommandLineOption(const FString& Key, FString& OutValue)
{
return GetCommandLineOption(FCommandLine::Get(), Key, OutValue);
}
bool UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(const TCHAR* CommandLine, const FString& Switch)
{
if (Switch.IsEmpty())
{
return false;
}
return FParse::Param(CommandLine, *Switch);
}
bool UDirectiveUtilFunctionLibrary::GetCommandLineOption(const TCHAR* CommandLine, const FString& Key, FString& OutValue)
{
OutValue.Reset();
if (Key.IsEmpty())
{
return false;
}
return FParse::Value(CommandLine, *(Key + TEXT("=")), OutValue);
}

View File

@@ -0,0 +1,164 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilGameplayTagFunctionLibrary.h"
#include "GameplayTagsManager.h"
FGameplayTag UDirectiveUtilGameplayTagFunctionLibrary::GetTagDirectParent(const FGameplayTag& Tag)
{
if (!Tag.IsValid())
{
return FGameplayTag();
}
return Tag.RequestDirectParent();
}
FGameplayTagContainer UDirectiveUtilGameplayTagFunctionLibrary::GetTagParents(const FGameplayTag& Tag)
{
if (!Tag.IsValid())
{
return FGameplayTagContainer();
}
FGameplayTagContainer Parents = Tag.GetGameplayTagParents();
Parents.RemoveTag(Tag);
return Parents;
}
int32 UDirectiveUtilGameplayTagFunctionLibrary::GetTagDepth(const FGameplayTag& Tag)
{
return GetTagSegments(Tag).Num();
}
FString UDirectiveUtilGameplayTagFunctionLibrary::GetTagLeafName(const FGameplayTag& Tag)
{
const TArray<FString> Segments = GetTagSegments(Tag);
return Segments.Num() > 0 ? Segments.Last() : FString();
}
TArray<FString> UDirectiveUtilGameplayTagFunctionLibrary::GetTagSegments(const FGameplayTag& Tag)
{
TArray<FString> Segments;
if (!Tag.IsValid())
{
return Segments;
}
Tag.GetTagName().ToString().ParseIntoArray(Segments, TEXT("."), true);
return Segments;
}
FGameplayTagContainer UDirectiveUtilGameplayTagFunctionLibrary::GetTagChildren(const FGameplayTag& Tag)
{
if (!Tag.IsValid())
{
return FGameplayTagContainer();
}
return UGameplayTagsManager::Get().RequestGameplayTagChildren(Tag);
}
FGameplayTagContainer UDirectiveUtilGameplayTagFunctionLibrary::GetTagDirectChildren(const FGameplayTag& Tag)
{
FGameplayTagContainer DirectChildren;
if (!Tag.IsValid())
{
return DirectChildren;
}
const int32 DirectChildDepth = GetTagDepth(Tag) + 1;
for (const FGameplayTag& Child : GetTagChildren(Tag))
{
if (GetTagDepth(Child) == DirectChildDepth)
{
DirectChildren.AddTag(Child);
}
}
return DirectChildren;
}
FGameplayTag UDirectiveUtilGameplayTagFunctionLibrary::GetTagCommonAncestor(const FGameplayTag& TagA, const FGameplayTag& TagB)
{
const TArray<FString> SegmentsA = GetTagSegments(TagA);
const TArray<FString> SegmentsB = GetTagSegments(TagB);
FString Prefix;
for (int32 Index = 0; Index < SegmentsA.Num() && Index < SegmentsB.Num(); ++Index)
{
if (!SegmentsA[Index].Equals(SegmentsB[Index]))
{
break;
}
if (!Prefix.IsEmpty())
{
Prefix += TEXT(".");
}
Prefix += SegmentsA[Index];
}
if (Prefix.IsEmpty())
{
return FGameplayTag();
}
// A common prefix of two registered tags is itself registered (parents auto-register).
return FGameplayTag::RequestGameplayTag(FName(*Prefix), false);
}
FGameplayTag UDirectiveUtilGameplayTagFunctionLibrary::GetTagAtDepth(const FGameplayTag& Tag, const int32 Depth)
{
if (!Tag.IsValid() || Depth < 1)
{
return FGameplayTag();
}
const TArray<FString> Segments = GetTagSegments(Tag);
if (Depth >= Segments.Num())
{
return Tag;
}
FString Prefix = Segments[0];
for (int32 Index = 1; Index < Depth; ++Index)
{
Prefix += TEXT(".");
Prefix += Segments[Index];
}
// An ancestor of a registered tag is always registered itself (parents auto-register).
return FGameplayTag::RequestGameplayTag(FName(*Prefix), false);
}
FGameplayTagContainer UDirectiveUtilGameplayTagFunctionLibrary::GetTagSiblings(const FGameplayTag& Tag)
{
const FGameplayTag DirectParent = GetTagDirectParent(Tag);
if (!DirectParent.IsValid())
{
return FGameplayTagContainer();
}
FGameplayTagContainer Siblings = GetTagDirectChildren(DirectParent);
Siblings.RemoveTag(Tag);
return Siblings;
}
bool UDirectiveUtilGameplayTagFunctionLibrary::IsLeafTag(const FGameplayTag& Tag)
{
return Tag.IsValid() && GetTagChildren(Tag).IsEmpty();
}
TArray<FGameplayTag> UDirectiveUtilGameplayTagFunctionLibrary::FindRegisteredTags(const FString& Substring)
{
TArray<FGameplayTag> MatchingTags;
if (Substring.IsEmpty())
{
return MatchingTags;
}
FGameplayTagContainer AllTags;
UGameplayTagsManager::Get().RequestAllGameplayTags(AllTags, /*OnlyIncludeDictionaryTags*/ false);
for (const FGameplayTag& RegisteredTag : AllTags)
{
if (RegisteredTag.GetTagName().ToString().Contains(Substring))
{
MatchingTags.Add(RegisteredTag);
}
}
return MatchingTags;
}

View File

@@ -0,0 +1,218 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilInputFunctionLibrary.h"
#include "EnhancedInputSubsystems.h"
#include "InputMappingContext.h"
#include "DirectiveUtilLogChannels.h"
#include "Logging/StructuredLog.h"
#include "GameFramework/Controller.h"
#include "GameFramework/PlayerController.h"
#include "Engine/LocalPlayer.h"
bool UDirectiveUtilInputFunctionLibrary::TryGetEnhancedInputSubsystemFromController(
AController* PlayerController,
UEnhancedInputLocalPlayerSubsystem*& EnhancedInput)
{
if (!PlayerController)
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("PlayerController is null. Cannot set input mapping contexts."));
return false;
}
const APlayerController* PC = Cast<APlayerController>(PlayerController);
if (!PC)
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller is not a PlayerController. Cannot get enhanced input subsystem."));
return false;
}
const ULocalPlayer* LocalPlayer = PC->GetLocalPlayer();
if (!LocalPlayer)
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("LocalPlayer not found. Cannot set input mapping contexts."));
return false;
}
EnhancedInput = LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>();
if(!EnhancedInput)
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("EnhancedInput subsystem not found. Cannot remove input mapping contexts."));
return false;
}
return true;
}
EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::AddInputMappingContexts(
AController* PlayerController,
const TArray<FDirectiveUtilEnhancedInputContextData>& Contexts,
const bool bClearPrevious)
{
if (Contexts.IsEmpty()) { return EDirectiveUtilSuccessStatus::Failure; }
UEnhancedInputLocalPlayerSubsystem* EnhancedInput;
const bool bEnhancedInputRetrievedFromController = TryGetEnhancedInputSubsystemFromController(PlayerController, EnhancedInput);
if(!bEnhancedInputRetrievedFromController)
{
return EDirectiveUtilSuccessStatus::Failure;
}
TArray<TPair<const UInputMappingContext*, int32>> LoadedContexts;
TArray<int32> FailedIndices;
for (int32 Index = 0; Index < Contexts.Num(); ++Index)
{
const auto& [InputContext, Priority] = Contexts[Index];
if (const UInputMappingContext* MappingContext = InputContext.LoadSynchronous())
{
LoadedContexts.Emplace(MappingContext, Priority);
}
else
{
FailedIndices.Add(Index);
}
}
if (FailedIndices.Num() > 0)
{
FString FailedIndicesStr = FString::JoinBy(FailedIndices, TEXT(", "), [](const int32 Index) { return FString::Printf(TEXT("%d"), Index); });
UE_LOGFMT(LogDirectiveUtil, Warning, "{FailedIndicies} Input Mapping Contexts failed to load and were not added! The failed indexes are [{FailedIndicieIndexes}]", FailedIndices.Num(), FailedIndicesStr);
}
if (LoadedContexts.IsEmpty())
{
return EDirectiveUtilSuccessStatus::Failure;
}
if (bClearPrevious)
{
EnhancedInput->ClearAllMappings();
}
for (const auto& [MappingContext, Priority] : LoadedContexts)
{
EnhancedInput->AddMappingContext(MappingContext, Priority);
}
UE_LOGFMT(LogDirectiveUtil, Verbose, "{MappingCount} Input mapping contexts set successfully.", Contexts.Num() - FailedIndices.Num());
return EDirectiveUtilSuccessStatus::Success;
}
EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::RemoveInputMappingContexts(
AController* PlayerController,
const TArray<TSoftObjectPtr<UInputMappingContext>>& Contexts)
{
if (Contexts.IsEmpty()) { return EDirectiveUtilSuccessStatus::Failure;; }
UEnhancedInputLocalPlayerSubsystem* EnhancedInput;
const bool bEnhancedInputRetrievedFromController = TryGetEnhancedInputSubsystemFromController(PlayerController, EnhancedInput);
if(!bEnhancedInputRetrievedFromController)
{
return EDirectiveUtilSuccessStatus::Failure;
}
TArray<int32> FailedIndices;
for (int32 Index = 0; Index < Contexts.Num(); ++Index)
{
const TSoftObjectPtr<UInputMappingContext>& Context = Contexts[Index];
if (const UInputMappingContext* MappingContext = Context.LoadSynchronous())
{
EnhancedInput->RemoveMappingContext(MappingContext);
} else
{
FailedIndices.Add(Index);
}
}
if (FailedIndices.Num() > 0)
{
FString FailedIndicesStr = FString::JoinBy(FailedIndices, TEXT(", "), [](const int32 Index) { return FString::Printf(TEXT("%d"), Index); });
UE_LOGFMT(LogDirectiveUtil, Warning, "{FailedIndicies} Input Mapping Contexts failed to load and were not removed! The failed indexes are [{FailedIndicieIndexes}]", FailedIndices.Num(), FailedIndicesStr);
}
if (FailedIndices.Num() == Contexts.Num())
{
return EDirectiveUtilSuccessStatus::Failure;
}
UE_LOGFMT(LogDirectiveUtil, Verbose, "{MappingCount} Input mapping contexts removed successfully.", Contexts.Num() - FailedIndices.Num());
return EDirectiveUtilSuccessStatus::Success;
}
EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::SwapInputMappingContexts(
AController* PlayerController,
const TSoftObjectPtr<UInputMappingContext> PreviousContext,
const TSoftObjectPtr<UInputMappingContext> NewContext,
const int32 Priority,
const bool bUsePreviousPriority)
{
const UInputMappingContext* LoadedPreviousMappingContext = PreviousContext.LoadSynchronous();
const UInputMappingContext* LoadedNewMappingContext = NewContext.LoadSynchronous();
if (!LoadedPreviousMappingContext || !LoadedNewMappingContext)
{
UE_LOGFMT(LogDirectiveUtil, Warning, "Both the previous and new input mapping contexts must be valid.");
return EDirectiveUtilSuccessStatus::Failure;
}
UEnhancedInputLocalPlayerSubsystem* EnhancedInput;
if (!TryGetEnhancedInputSubsystemFromController(PlayerController, EnhancedInput))
{
return EDirectiveUtilSuccessStatus::Failure;
}
if (int32 PreviousPriority; EnhancedInput->HasMappingContext(LoadedPreviousMappingContext, PreviousPriority))
{
const int32 TargetPriority = bUsePreviousPriority ? PreviousPriority : Priority;
EnhancedInput->RemoveMappingContext(LoadedPreviousMappingContext);
EnhancedInput->AddMappingContext(LoadedNewMappingContext, TargetPriority);
UE_LOGFMT(LogDirectiveUtil, Verbose, "Input mapping contexts {PreviousContext} and {NewContext} swapped successfully at priority {Priority}.", LoadedPreviousMappingContext->GetName(), LoadedNewMappingContext->GetName(), TargetPriority);
}
else
{
EnhancedInput->AddMappingContext(LoadedNewMappingContext, Priority);
UE_LOGFMT(LogDirectiveUtil, Warning, "Previous input mapping context {PreviousContext} not found. New context {NewContext} added at priority {BackupPriority}.", LoadedPreviousMappingContext->GetName(), LoadedNewMappingContext->GetName(), Priority);
}
return EDirectiveUtilSuccessStatus::Success;
}
UEnhancedInputLocalPlayerSubsystem* UDirectiveUtilInputFunctionLibrary::GetEnhancedInputSubsystem(AController* PlayerController)
{
UEnhancedInputLocalPlayerSubsystem* EnhancedInput = nullptr;
TryGetEnhancedInputSubsystemFromController(PlayerController, EnhancedInput);
return EnhancedInput;
}
bool UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(AController* PlayerController, TSoftObjectPtr<UInputMappingContext> Context)
{
const UInputMappingContext* MappingContext = Context.LoadSynchronous();
if (!MappingContext)
{
return false;
}
UEnhancedInputLocalPlayerSubsystem* EnhancedInput;
if (!TryGetEnhancedInputSubsystemFromController(PlayerController, EnhancedInput))
{
return false;
}
int32 OutPriority;
return EnhancedInput->HasMappingContext(MappingContext, OutPriority);
}
EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::ClearAllInputMappingContexts(AController* PlayerController)
{
UEnhancedInputLocalPlayerSubsystem* EnhancedInput;
if (!TryGetEnhancedInputSubsystemFromController(PlayerController, EnhancedInput))
{
return EDirectiveUtilSuccessStatus::Failure;
}
EnhancedInput->ClearAllMappings();
return EDirectiveUtilSuccessStatus::Success;
}

View File

@@ -0,0 +1,205 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilMapFunctionLibrary.h"
void UDirectiveUtilMapFunctionLibrary::Map_FindOrAdd(const TMap<int32, int32>& TargetMap, const int32& Key, int32& Value)
{
checkNoEntry();
}
void UDirectiveUtilMapFunctionLibrary::Map_ClearValues(const TMap<int32, int32>& TargetMap)
{
checkNoEntry();
}
void UDirectiveUtilMapFunctionLibrary::Map_GetKeysByValue(const TMap<int32, int32>& TargetMap, const int32& Value, TArray<int32>& Keys)
{
checkNoEntry();
}
bool UDirectiveUtilMapFunctionLibrary::Map_HasValue(const TMap<int32, int32>& TargetMap, const int32& Value)
{
checkNoEntry();
return false;
}
int32 UDirectiveUtilMapFunctionLibrary::Map_RemoveKeys(const TMap<int32, int32>& TargetMap, const TArray<int32>& Keys)
{
checkNoEntry();
return 0;
}
void UDirectiveUtilMapFunctionLibrary::Map_Append(const TMap<int32, int32>& TargetMap, const TMap<int32, int32>& SourceMap, bool bOverwriteExisting)
{
checkNoEntry();
}
void UDirectiveUtilMapFunctionLibrary::GenericMap_FindOrAdd(
const void* TargetMap,
const FMapProperty* MapProperty,
const void* KeyPtr,
void* ValuePtr)
{
if (!TargetMap || !MapProperty || !KeyPtr)
{
return;
}
FScriptMapHelper MapHelper(MapProperty, TargetMap);
void* ValueInMap = MapHelper.FindOrAdd(KeyPtr);
if (ValuePtr && ValueInMap)
{
MapProperty->ValueProp->CopyCompleteValueFromScriptVM(ValuePtr, ValueInMap);
}
}
void UDirectiveUtilMapFunctionLibrary::GenericMap_ClearValues(
const void* TargetMap,
const FMapProperty* MapProperty)
{
if (!TargetMap || !MapProperty)
{
return;
}
FScriptMapHelper MapHelper(MapProperty, TargetMap);
const FProperty* ValueProp = MapProperty->ValueProp;
const int32 MaxIndex = MapHelper.GetMaxIndex();
for (int32 InternalIndex = 0; InternalIndex < MaxIndex; ++InternalIndex)
{
if (MapHelper.IsValidIndex(InternalIndex))
{
ValueProp->ClearValue(MapHelper.GetValuePtr(InternalIndex));
}
}
}
void UDirectiveUtilMapFunctionLibrary::GenericMap_GetKeysByValue(
const void* TargetMap,
const FMapProperty* MapProperty,
const void* ValuePtr,
const void* TargetArray,
const FArrayProperty* ArrayProperty)
{
if (!TargetArray || !ArrayProperty)
{
return;
}
FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
ArrayHelper.EmptyValues();
if (!TargetMap || !MapProperty || !ValuePtr || !ArrayProperty->Inner->SameType(MapProperty->KeyProp))
{
return;
}
FScriptMapHelper MapHelper(MapProperty, TargetMap);
const FProperty* ValueProp = MapProperty->ValueProp;
const FProperty* InnerProp = ArrayProperty->Inner;
const int32 MaxIndex = MapHelper.GetMaxIndex();
for (int32 InternalIndex = 0; InternalIndex < MaxIndex; ++InternalIndex)
{
if (MapHelper.IsValidIndex(InternalIndex) && ValueProp->Identical(MapHelper.GetValuePtr(InternalIndex), ValuePtr))
{
const int32 LastIndex = ArrayHelper.AddValue();
InnerProp->CopySingleValueToScriptVM(ArrayHelper.GetRawPtr(LastIndex), MapHelper.GetKeyPtr(InternalIndex));
}
}
}
bool UDirectiveUtilMapFunctionLibrary::GenericMap_HasValue(
const void* TargetMap,
const FMapProperty* MapProperty,
const void* ValuePtr)
{
if (!TargetMap || !MapProperty || !ValuePtr)
{
return false;
}
FScriptMapHelper MapHelper(MapProperty, TargetMap);
const FProperty* ValueProp = MapProperty->ValueProp;
const int32 MaxIndex = MapHelper.GetMaxIndex();
for (int32 InternalIndex = 0; InternalIndex < MaxIndex; ++InternalIndex)
{
if (MapHelper.IsValidIndex(InternalIndex) && ValueProp->Identical(MapHelper.GetValuePtr(InternalIndex), ValuePtr))
{
return true;
}
}
return false;
}
int32 UDirectiveUtilMapFunctionLibrary::GenericMap_RemoveKeys(
const void* TargetMap,
const FMapProperty* MapProperty,
const void* TargetArray,
const FArrayProperty* ArrayProperty)
{
if (!TargetMap || !MapProperty || !TargetArray || !ArrayProperty
|| !ArrayProperty->Inner->SameType(MapProperty->KeyProp))
{
return 0;
}
FScriptMapHelper MapHelper(MapProperty, TargetMap);
FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
int32 NumRemoved = 0;
const int32 Num = ArrayHelper.Num();
for (int32 Index = 0; Index < Num; ++Index)
{
if (MapHelper.RemovePair(ArrayHelper.GetRawPtr(Index)))
{
++NumRemoved;
}
}
return NumRemoved;
}
void UDirectiveUtilMapFunctionLibrary::GenericMap_Append(
const void* TargetMap,
const FMapProperty* TargetMapProperty,
const void* SourceMap,
const FMapProperty* SourceMapProperty,
const bool bOverwriteExisting)
{
if (!TargetMap || !TargetMapProperty || !SourceMap || !SourceMapProperty
|| !TargetMapProperty->KeyProp->SameType(SourceMapProperty->KeyProp)
|| !TargetMapProperty->ValueProp->SameType(SourceMapProperty->ValueProp))
{
return;
}
// Appending a map onto itself is a no-op; bail before AddPair can reallocate under the source pointers.
if (TargetMap == SourceMap)
{
return;
}
FScriptMapHelper TargetHelper(TargetMapProperty, TargetMap);
FScriptMapHelper SourceHelper(SourceMapProperty, SourceMap);
const int32 MaxIndex = SourceHelper.GetMaxIndex();
for (int32 InternalIndex = 0; InternalIndex < MaxIndex; ++InternalIndex)
{
if (!SourceHelper.IsValidIndex(InternalIndex))
{
continue;
}
const uint8* KeyPtr = SourceHelper.GetKeyPtr(InternalIndex);
if (bOverwriteExisting || !TargetHelper.FindValueFromHash(KeyPtr))
{
TargetHelper.AddPair(KeyPtr, SourceHelper.GetValuePtr(InternalIndex));
}
}
}

View File

@@ -0,0 +1,466 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilMathFunctionLibrary.h"
namespace
{
double EaseBackIn(double t)
{
const double s = 1.70158;
return t * t * ((s + 1.0) * t - s);
}
double EaseBackOut(double t)
{
const double s = 1.70158;
t -= 1.0;
return t * t * ((s + 1.0) * t + s) + 1.0;
}
double EaseBackInOut(double t)
{
const double s = 1.70158 * 1.525;
t *= 2.0;
if (t < 1.0)
{
return 0.5 * (t * t * ((s + 1.0) * t - s));
}
t -= 2.0;
return 0.5 * (t * t * ((s + 1.0) * t + s) + 2.0);
}
double EaseElasticIn(double t)
{
if (t <= 0.0) { return 0.0; }
if (t >= 1.0) { return 1.0; }
const double p = 0.3;
const double s = p / 4.0;
t -= 1.0;
return -(FMath::Pow(2.0, 10.0 * t) * FMath::Sin((t - s) * (2.0 * PI) / p));
}
double EaseElasticOut(double t)
{
if (t <= 0.0) { return 0.0; }
if (t >= 1.0) { return 1.0; }
const double p = 0.3;
const double s = p / 4.0;
return FMath::Pow(2.0, -10.0 * t) * FMath::Sin((t - s) * (2.0 * PI) / p) + 1.0;
}
double EaseElasticInOut(double t)
{
if (t <= 0.0) { return 0.0; }
if (t >= 1.0) { return 1.0; }
const double p = 0.3 * 1.5;
const double s = p / 4.0;
t *= 2.0;
if (t < 1.0)
{
t -= 1.0;
return -0.5 * (FMath::Pow(2.0, 10.0 * t) * FMath::Sin((t - s) * (2.0 * PI) / p));
}
t -= 1.0;
return FMath::Pow(2.0, -10.0 * t) * FMath::Sin((t - s) * (2.0 * PI) / p) * 0.5 + 1.0;
}
double EaseBounceOut(double t)
{
const double n1 = 7.5625;
const double d1 = 2.75;
if (t < 1.0 / d1)
{
return n1 * t * t;
}
if (t < 2.0 / d1)
{
t -= 1.5 / d1;
return n1 * t * t + 0.75;
}
if (t < 2.5 / d1)
{
t -= 2.25 / d1;
return n1 * t * t + 0.9375;
}
t -= 2.625 / d1;
return n1 * t * t + 0.984375;
}
double EaseBounceIn(double t)
{
return 1.0 - EaseBounceOut(1.0 - t);
}
double EaseBounceInOut(double t)
{
return t < 0.5
? (1.0 - EaseBounceOut(1.0 - 2.0 * t)) * 0.5
: (1.0 + EaseBounceOut(2.0 * t - 1.0)) * 0.5;
}
}
float UDirectiveUtilMathFunctionLibrary::PerlinNoise2D(const FVector2D Position)
{
return FMath::PerlinNoise2D(Position);
}
float UDirectiveUtilMathFunctionLibrary::PerlinNoise3D(const FVector& Position)
{
return FMath::PerlinNoise3D(Position);
}
float UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(const FVector& A, const FVector& B)
{
return FMath::RadiansToDegrees(FMath::Acos(FMath::Clamp(FVector::DotProduct(A.GetSafeNormal(), B.GetSafeNormal()), -1.0, 1.0)));
}
float UDirectiveUtilMathFunctionLibrary::EaseAlpha(const float Alpha, const EDirectiveUtilEaseType EaseType)
{
const double t = static_cast<double>(FMath::Clamp(Alpha, 0.0f, 1.0f));
double Result = t;
switch (EaseType)
{
case EDirectiveUtilEaseType::BackIn: Result = EaseBackIn(t); break;
case EDirectiveUtilEaseType::BackOut: Result = EaseBackOut(t); break;
case EDirectiveUtilEaseType::BackInOut: Result = EaseBackInOut(t); break;
case EDirectiveUtilEaseType::ElasticIn: Result = EaseElasticIn(t); break;
case EDirectiveUtilEaseType::ElasticOut: Result = EaseElasticOut(t); break;
case EDirectiveUtilEaseType::ElasticInOut: Result = EaseElasticInOut(t); break;
case EDirectiveUtilEaseType::BounceIn: Result = EaseBounceIn(t); break;
case EDirectiveUtilEaseType::BounceOut: Result = EaseBounceOut(t); break;
case EDirectiveUtilEaseType::BounceInOut: Result = EaseBounceInOut(t); break;
}
return static_cast<float>(Result);
}
float UDirectiveUtilMathFunctionLibrary::EaseFloat(const float A, const float B, const float Alpha, const EDirectiveUtilEaseType EaseType)
{
return FMath::Lerp(A, B, EaseAlpha(Alpha, EaseType));
}
FVector UDirectiveUtilMathFunctionLibrary::EaseVector(const FVector& A, const FVector& B, const float Alpha, const EDirectiveUtilEaseType EaseType)
{
return FMath::Lerp(A, B, static_cast<double>(EaseAlpha(Alpha, EaseType)));
}
FRotator UDirectiveUtilMathFunctionLibrary::EaseRotator(const FRotator& A, const FRotator& B, const float Alpha, const EDirectiveUtilEaseType EaseType)
{
return FQuat::Slerp(A.Quaternion(), B.Quaternion(), EaseAlpha(Alpha, EaseType)).Rotator();
}
FLinearColor UDirectiveUtilMathFunctionLibrary::EaseColor(const FLinearColor& A, const FLinearColor& B, const float Alpha, const EDirectiveUtilEaseType EaseType)
{
return FMath::Lerp(A, B, EaseAlpha(Alpha, EaseType));
}
float UDirectiveUtilMathFunctionLibrary::RoundToDecimals(const float Value, int32 Decimals)
{
Decimals = FMath::Clamp(Decimals, 0, 10);
if (Decimals == 0)
{
return FMath::RoundHalfFromZero(Value);
}
const double Factor = FMath::Pow(10.0, static_cast<double>(Decimals));
return static_cast<float>(FMath::RoundHalfFromZero(static_cast<double>(Value) * Factor) / Factor);
}
FText UDirectiveUtilMathFunctionLibrary::RoundToDecimalsAsText(const float Value, int32 Decimals)
{
Decimals = FMath::Clamp(Decimals, 0, 10);
FNumberFormattingOptions Options;
Options.MinimumFractionalDigits = 0;
Options.MaximumFractionalDigits = Decimals;
Options.RoundingMode = ERoundingMode::HalfFromZero;
return FText::AsNumber(Value, &Options);
}
FText UDirectiveUtilMathFunctionLibrary::FormatBytes(const int64 Bytes, int32 Decimals)
{
Decimals = FMath::Clamp(Decimals, 0, 3);
static const TCHAR* Suffixes[] = { TEXT("B"), TEXT("KB"), TEXT("MB"), TEXT("GB"), TEXT("TB"), TEXT("PB") };
const bool bNegative = Bytes < 0;
double Value = FMath::Abs(static_cast<double>(Bytes));
int32 SuffixIndex = 0;
while (Value >= 1024.0 && SuffixIndex < UE_ARRAY_COUNT(Suffixes) - 1)
{
Value /= 1024.0;
++SuffixIndex;
}
return FText::FromString(FString::Printf(TEXT("%s%.*f %s"),
bNegative ? TEXT("-") : TEXT(""), SuffixIndex == 0 ? 0 : Decimals, Value, Suffixes[SuffixIndex]));
}
FText UDirectiveUtilMathFunctionLibrary::FormatDuration(const float Seconds, const bool bIncludeSeconds)
{
if (!FMath::IsFinite(Seconds))
{
return FText::FromString(TEXT("0s"));
}
const int64 TotalSeconds = static_cast<int64>(FMath::Abs(static_cast<double>(Seconds)));
const bool bNegative = Seconds < 0.0f && TotalSeconds > 0;
const int64 UnitValues[] = { TotalSeconds / 86400, (TotalSeconds / 3600) % 24, (TotalSeconds / 60) % 60, TotalSeconds % 60 };
static const TCHAR* UnitSuffixes[] = { TEXT("d"), TEXT("h"), TEXT("m"), TEXT("s") };
const int32 NumUnits = bIncludeSeconds ? 4 : 3;
int32 FirstUnit = NumUnits - 1;
for (int32 Index = 0; Index < NumUnits; ++Index)
{
if (UnitValues[Index] != 0)
{
FirstUnit = Index;
break;
}
}
int32 LastUnit = FirstUnit;
for (int32 Index = NumUnits - 1; Index >= FirstUnit; --Index)
{
if (UnitValues[Index] != 0)
{
LastUnit = Index;
break;
}
}
FString Result = bNegative ? TEXT("-") : TEXT("");
for (int32 Index = FirstUnit; Index <= LastUnit; ++Index)
{
if (Index == FirstUnit)
{
Result += FString::Printf(TEXT("%lld%s"), UnitValues[Index], UnitSuffixes[Index]);
}
else
{
Result += FString::Printf(TEXT(" %02lld%s"), UnitValues[Index], UnitSuffixes[Index]);
}
}
return FText::FromString(Result);
}
FText UDirectiveUtilMathFunctionLibrary::FormatRelativeTime(const FDateTime& Timestamp)
{
const FTimespan Delta = FDateTime::Now() - Timestamp;
const bool bFuture = Delta.GetTicks() < 0;
// Round to whole seconds so clock-adjacent inputs (e.g. Now() + 2 hours) land in the intended bucket.
const int64 SecondsAbs = static_cast<int64>(FMath::RoundToDouble(FMath::Abs(Delta.GetTotalSeconds())));
if (SecondsAbs < 60)
{
return FText::FromString(TEXT("just now"));
}
int64 Count;
const TCHAR* Unit;
if (SecondsAbs < 3600)
{
Count = SecondsAbs / 60;
Unit = TEXT("minute");
}
else if (SecondsAbs < 86400)
{
Count = SecondsAbs / 3600;
Unit = TEXT("hour");
}
else
{
Count = SecondsAbs / 86400;
Unit = TEXT("day");
}
const FString Quantity = FString::Printf(TEXT("%lld %s%s"), Count, Unit, Count == 1 ? TEXT("") : TEXT("s"));
return FText::FromString(bFuture
? FString::Printf(TEXT("in %s"), *Quantity)
: FString::Printf(TEXT("%s ago"), *Quantity));
}
int64 UDirectiveUtilMathFunctionLibrary::GetIntArraySum(const TArray<int32>& Values)
{
int64 Sum = 0;
for (const int32 Value : Values)
{
Sum += Value;
}
return Sum;
}
float UDirectiveUtilMathFunctionLibrary::GetIntArrayAverage(const TArray<int32>& Values)
{
if (Values.IsEmpty())
{
return 0.0f;
}
return static_cast<float>(static_cast<double>(GetIntArraySum(Values)) / Values.Num());
}
float UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian(const TArray<int32>& Values)
{
if (Values.IsEmpty())
{
return 0.0f;
}
TArray<int32> Sorted = Values;
Sorted.Sort();
const int32 Middle = Sorted.Num() / 2;
if (Sorted.Num() % 2 == 0)
{
return static_cast<float>((static_cast<double>(Sorted[Middle - 1]) + static_cast<double>(Sorted[Middle])) * 0.5);
}
return static_cast<float>(Sorted[Middle]);
}
float UDirectiveUtilMathFunctionLibrary::GetIntArrayStandardDeviation(const TArray<int32>& Values)
{
if (Values.IsEmpty())
{
return 0.0f;
}
const double Mean = static_cast<double>(GetIntArraySum(Values)) / Values.Num();
double SquaredDeltaSum = 0.0;
for (const int32 Value : Values)
{
const double Delta = static_cast<double>(Value) - Mean;
SquaredDeltaSum += Delta * Delta;
}
return static_cast<float>(FMath::Sqrt(SquaredDeltaSum / Values.Num()));
}
float UDirectiveUtilMathFunctionLibrary::GetFloatArraySum(const TArray<float>& Values)
{
double Sum = 0.0;
for (const float Value : Values)
{
Sum += static_cast<double>(Value);
}
return static_cast<float>(Sum);
}
float UDirectiveUtilMathFunctionLibrary::GetFloatArrayAverage(const TArray<float>& Values)
{
if (Values.IsEmpty())
{
return 0.0f;
}
double Sum = 0.0;
for (const float Value : Values)
{
Sum += static_cast<double>(Value);
}
return static_cast<float>(Sum / Values.Num());
}
float UDirectiveUtilMathFunctionLibrary::GetFloatArrayMedian(const TArray<float>& Values)
{
if (Values.IsEmpty())
{
return 0.0f;
}
TArray<float> Sorted = Values;
Sorted.Sort();
const int32 Middle = Sorted.Num() / 2;
if (Sorted.Num() % 2 == 0)
{
return static_cast<float>((static_cast<double>(Sorted[Middle - 1]) + static_cast<double>(Sorted[Middle])) * 0.5);
}
return Sorted[Middle];
}
float UDirectiveUtilMathFunctionLibrary::GetFloatArrayStandardDeviation(const TArray<float>& Values)
{
if (Values.IsEmpty())
{
return 0.0f;
}
double Sum = 0.0;
for (const float Value : Values)
{
Sum += static_cast<double>(Value);
}
const double Mean = Sum / Values.Num();
double SquaredDeltaSum = 0.0;
for (const float Value : Values)
{
const double Delta = static_cast<double>(Value) - Mean;
SquaredDeltaSum += Delta * Delta;
}
return static_cast<float>(FMath::Sqrt(SquaredDeltaSum / Values.Num()));
}
int32 UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights(const TArray<float>& Weights)
{
float Total = 0.0f;
for (const float Weight : Weights)
{
Total += FMath::Max(0.0f, Weight);
}
if (Total <= 0.0f)
{
return INDEX_NONE;
}
const float Roll = FMath::FRand() * Total;
float Accumulated = 0.0f;
int32 LastPositiveIndex = INDEX_NONE;
for (int32 Index = 0; Index < Weights.Num(); ++Index)
{
const float Weight = FMath::Max(0.0f, Weights[Index]);
if (Weight <= 0.0f)
{
continue;
}
LastPositiveIndex = Index;
Accumulated += Weight;
if (Roll < Accumulated)
{
return Index;
}
}
return LastPositiveIndex;
}
int32 UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeightsFromStream(FRandomStream& Stream, const TArray<float>& Weights)
{
float Total = 0.0f;
for (const float Weight : Weights)
{
Total += FMath::Max(0.0f, Weight);
}
if (Total <= 0.0f)
{
return INDEX_NONE;
}
const float Roll = Stream.FRand() * Total;
float Accumulated = 0.0f;
int32 LastPositiveIndex = INDEX_NONE;
for (int32 Index = 0; Index < Weights.Num(); ++Index)
{
const float Weight = FMath::Max(0.0f, Weights[Index]);
if (Weight <= 0.0f)
{
continue;
}
LastPositiveIndex = Index;
Accumulated += Weight;
if (Roll < Accumulated)
{
return Index;
}
}
return LastPositiveIndex;
}

View File

@@ -0,0 +1,131 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilRegexFunctionLibrary.h"
#include "Internationalization/Regex.h"
namespace
{
FRegexPattern MakePattern(const FString& Pattern, const bool bCaseSensitive)
{
return FRegexPattern(Pattern, bCaseSensitive ? ERegexPatternFlags::None : ERegexPatternFlags::CaseInsensitive);
}
}
bool UDirectiveUtilRegexFunctionLibrary::RegexMatches(const FString& Input, const FString& Pattern, const bool bCaseSensitive)
{
if (Pattern.IsEmpty())
{
return false;
}
const FRegexPattern RegexPattern = MakePattern(Pattern, bCaseSensitive);
FRegexMatcher Matcher(RegexPattern, Input);
return Matcher.FindNext();
}
bool UDirectiveUtilRegexFunctionLibrary::RegexFindFirst(const FString& Input, const FString& Pattern, FString& OutMatch, int32& OutMatchStart, int32& OutMatchEnd, const bool bCaseSensitive)
{
OutMatch = FString();
OutMatchStart = INDEX_NONE;
OutMatchEnd = INDEX_NONE;
if (Pattern.IsEmpty())
{
return false;
}
const FRegexPattern RegexPattern = MakePattern(Pattern, bCaseSensitive);
FRegexMatcher Matcher(RegexPattern, Input);
if (Matcher.FindNext())
{
OutMatchStart = Matcher.GetMatchBeginning();
OutMatchEnd = Matcher.GetMatchEnding();
OutMatch = Matcher.GetCaptureGroup(0);
return true;
}
return false;
}
TArray<FString> UDirectiveUtilRegexFunctionLibrary::RegexFindAll(const FString& Input, const FString& Pattern, const bool bCaseSensitive)
{
TArray<FString> Matches;
if (Pattern.IsEmpty())
{
return Matches;
}
const FRegexPattern RegexPattern = MakePattern(Pattern, bCaseSensitive);
FRegexMatcher Matcher(RegexPattern, Input);
while (Matcher.FindNext())
{
const int32 Begin = Matcher.GetMatchBeginning();
const int32 End = Matcher.GetMatchEnding();
if (Begin == End)
{
continue;
}
Matches.Add(Matcher.GetCaptureGroup(0));
}
return Matches;
}
FString UDirectiveUtilRegexFunctionLibrary::RegexReplaceAll(const FString& Input, const FString& Pattern, const FString& Replacement, const bool bCaseSensitive)
{
if (Pattern.IsEmpty())
{
return Input;
}
const FRegexPattern RegexPattern = MakePattern(Pattern, bCaseSensitive);
FRegexMatcher Matcher(RegexPattern, Input);
FString Result;
int32 LastEnd = 0;
while (Matcher.FindNext())
{
const int32 Begin = Matcher.GetMatchBeginning();
const int32 End = Matcher.GetMatchEnding();
if (Begin == End)
{
continue;
}
if (Begin < LastEnd)
{
continue;
}
Result.Append(Input.Mid(LastEnd, Begin - LastEnd));
Result.Append(Replacement);
LastEnd = End;
}
Result.Append(Input.Mid(LastEnd));
return Result;
}
bool UDirectiveUtilRegexFunctionLibrary::RegexGetCaptureGroup(const FString& Input, const FString& Pattern, const int32 GroupIndex, FString& OutGroup, const bool bCaseSensitive)
{
OutGroup = FString();
if (Pattern.IsEmpty() || GroupIndex < 0)
{
return false;
}
const FRegexPattern RegexPattern = MakePattern(Pattern, bCaseSensitive);
FRegexMatcher Matcher(RegexPattern, Input);
if (!Matcher.FindNext())
{
return false;
}
if (Matcher.GetCaptureGroupBeginning(GroupIndex) == INDEX_NONE)
{
return false;
}
OutGroup = Matcher.GetCaptureGroup(GroupIndex);
return true;
}

View File

@@ -0,0 +1,121 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilSaveGameFunctionLibrary.h"
#include "Libraries/DirectiveUtilStringFunctionLibrary.h"
#include "Kismet/GameplayStatics.h"
#include "GameFramework/SaveGame.h"
#include "HAL/FileManager.h"
#include "Misc/Paths.h"
namespace
{
FString GetSaveGamesDirectory()
{
return FPaths::ProjectSavedDir() / TEXT("SaveGames");
}
FString GetSaveSlotFilePath(const FString& SlotName)
{
return GetSaveGamesDirectory() / (SlotName + TEXT(".sav"));
}
bool IsValidSaveSlotName(const FString& SlotName)
{
return UDirectiveUtilStringFunctionLibrary::IsValidFileName(SlotName);
}
}
TArray<FString> UDirectiveUtilSaveGameFunctionLibrary::GetAllSaveSlotNames()
{
TArray<FString> SlotNames;
TArray<FString> Files;
IFileManager::Get().FindFiles(Files, *(GetSaveGamesDirectory() / TEXT("*.sav")), true, false);
SlotNames.Reserve(Files.Num());
for (const FString& File : Files)
{
SlotNames.Add(FPaths::GetBaseFilename(File));
}
return SlotNames;
}
bool UDirectiveUtilSaveGameFunctionLibrary::GetSaveSlotTimestamp(const FString& SlotName, FDateTime& OutTimestamp)
{
OutTimestamp = FDateTime();
if (!IsValidSaveSlotName(SlotName))
{
return false;
}
const FDateTime Timestamp = IFileManager::Get().GetTimeStamp(*GetSaveSlotFilePath(SlotName));
if (Timestamp == FDateTime::MinValue())
{
return false;
}
OutTimestamp = Timestamp + (FDateTime::Now() - FDateTime::UtcNow());
return true;
}
bool UDirectiveUtilSaveGameFunctionLibrary::SaveGameToBytes(USaveGame* SaveGameObject, TArray<uint8>& OutBytes)
{
OutBytes.Reset();
if (!SaveGameObject)
{
return false;
}
return UGameplayStatics::SaveGameToMemory(SaveGameObject, OutBytes);
}
USaveGame* UDirectiveUtilSaveGameFunctionLibrary::LoadGameFromBytes(const TArray<uint8>& SaveData)
{
if (SaveData.Num() == 0)
{
return nullptr;
}
return UGameplayStatics::LoadGameFromMemory(SaveData);
}
bool UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(const FString& SlotName, const int32 UserIndex)
{
if (!IsValidSaveSlotName(SlotName))
{
return false;
}
return UGameplayStatics::DoesSaveGameExist(SlotName, UserIndex);
}
bool UDirectiveUtilSaveGameFunctionLibrary::DeleteSaveSlot(const FString& SlotName, const int32 UserIndex)
{
if (!IsValidSaveSlotName(SlotName))
{
return false;
}
return UGameplayStatics::DeleteGameInSlot(SlotName, UserIndex);
}
bool UDirectiveUtilSaveGameFunctionLibrary::RenameSaveSlot(const FString& OldSlotName, const FString& NewSlotName, const int32 UserIndex)
{
if (!IsValidSaveSlotName(OldSlotName) || !IsValidSaveSlotName(NewSlotName) || OldSlotName == NewSlotName)
{
return false;
}
if (!UGameplayStatics::DoesSaveGameExist(OldSlotName, UserIndex) || UGameplayStatics::DoesSaveGameExist(NewSlotName, UserIndex))
{
return false;
}
TArray<uint8> SaveData;
if (!UGameplayStatics::LoadDataFromSlot(SaveData, OldSlotName, UserIndex))
{
return false;
}
if (!UGameplayStatics::SaveDataToSlot(SaveData, NewSlotName, UserIndex))
{
return false;
}
// If this delete fails the new copy is kept alongside the original, so the save is never lost.
return UGameplayStatics::DeleteGameInSlot(OldSlotName, UserIndex);
}

View File

@@ -0,0 +1,422 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilStringFunctionLibrary.h"
#include "Misc/Base64.h"
#include "Misc/Crc.h"
#include "Misc/Paths.h"
#include "Misc/SecureHash.h"
namespace
{
TArray<uint8> StringToUtf8Bytes(const FString& String)
{
const FTCHARToUTF8 Converter(*String, String.Len());
return TArray<uint8>(reinterpret_cast<const uint8*>(Converter.Get()), Converter.Length());
}
}
bool UDirectiveUtilStringFunctionLibrary::ContainsLetters(const FString& String)
{
for (const TCHAR& Char : String)
{
if (FChar::IsAlpha(Char))
{
return true;
}
}
return false;
}
bool UDirectiveUtilStringFunctionLibrary::ContainsNumbers(const FString& String)
{
for (const TCHAR& Char : String)
{
if (FChar::IsDigit(Char))
{
return true;
}
}
return false;
}
bool UDirectiveUtilStringFunctionLibrary::ContainsSpaces(const FString& String)
{
for (const TCHAR& Char : String)
{
if (FChar::IsWhitespace(Char))
{
return true;
}
}
return false;
}
bool UDirectiveUtilStringFunctionLibrary::ContainsSpecialCharacters(const FString& String)
{
for (const TCHAR& Char : String)
{
if (FChar::IsPunct(Char))
{
return true;
}
}
return false;
}
FString UDirectiveUtilStringFunctionLibrary::FilterCharacters(
const FString& String,
const bool bLetters,
const bool bNumbers,
const bool bSpecialCharacters,
const bool bSpaces)
{
FString NewString;
NewString.Reserve(String.Len());
for (const TCHAR& Char : String)
{
if (bLetters && FChar::IsAlpha(Char)) continue;
if (bNumbers && FChar::IsDigit(Char)) continue;
if (bSpecialCharacters && FChar::IsPunct(Char)) continue;
if (bSpaces && FChar::IsWhitespace(Char)) continue;
NewString.AppendChar(Char);
}
return NewString;
}
FString UDirectiveUtilStringFunctionLibrary::TruncateString(const FString& String, const int32 MaxLength, const FString& Suffix)
{
if (String.Len() <= MaxLength)
{
return String;
}
return String.Left(FMath::Max(0, MaxLength - Suffix.Len())) + Suffix;
}
FString UDirectiveUtilStringFunctionLibrary::ToTitleCase(const FString& String)
{
FString Result;
Result.Reserve(String.Len());
bool bCapitalizeNext = true;
for (const TCHAR& Char : String)
{
if (FChar::IsWhitespace(Char))
{
bCapitalizeNext = true;
Result.AppendChar(Char);
}
else if (bCapitalizeNext && FChar::IsAlpha(Char))
{
Result.AppendChar(FChar::ToUpper(Char));
bCapitalizeNext = false;
}
else
{
Result.AppendChar(FChar::ToLower(Char));
bCapitalizeNext = false;
}
}
return Result;
}
TArray<FString> UDirectiveUtilStringFunctionLibrary::SplitIntoWords(const FString& String)
{
TArray<FString> Words;
FString CurrentWord;
for (int32 Index = 0; Index < String.Len(); ++Index)
{
const TCHAR Char = String[Index];
if (!FChar::IsAlnum(Char))
{
if (!CurrentWord.IsEmpty())
{
Words.Add(MoveTemp(CurrentWord));
CurrentWord.Reset();
}
continue;
}
if (!CurrentWord.IsEmpty())
{
const TCHAR Previous = CurrentWord[CurrentWord.Len() - 1];
const bool bNextIsLower = Index + 1 < String.Len() && FChar::IsLower(String[Index + 1]);
const bool bBoundary =
(FChar::IsLower(Previous) && FChar::IsUpper(Char)) ||
(FChar::IsUpper(Previous) && FChar::IsUpper(Char) && bNextIsLower) ||
(FChar::IsAlpha(Previous) && FChar::IsDigit(Char)) ||
(FChar::IsDigit(Previous) && FChar::IsAlpha(Char));
if (bBoundary)
{
Words.Add(MoveTemp(CurrentWord));
CurrentWord.Reset();
}
}
CurrentWord.AppendChar(Char);
}
if (!CurrentWord.IsEmpty())
{
Words.Add(MoveTemp(CurrentWord));
}
return Words;
}
FString UDirectiveUtilStringFunctionLibrary::ToCamelCase(const FString& String)
{
const TArray<FString> Words = SplitIntoWords(String);
FString Result;
for (int32 Index = 0; Index < Words.Num(); ++Index)
{
FString Word = Words[Index].ToLower();
if (Index > 0)
{
Word[0] = FChar::ToUpper(Word[0]);
}
Result += Word;
}
return Result;
}
FString UDirectiveUtilStringFunctionLibrary::ToPascalCase(const FString& String)
{
FString Result;
for (const FString& Word : SplitIntoWords(String))
{
FString Cased = Word.ToLower();
Cased[0] = FChar::ToUpper(Cased[0]);
Result += Cased;
}
return Result;
}
FString UDirectiveUtilStringFunctionLibrary::ToSnakeCase(const FString& String)
{
return FString::Join(SplitIntoWords(String), TEXT("_")).ToLower();
}
FString UDirectiveUtilStringFunctionLibrary::ToKebabCase(const FString& String)
{
return FString::Join(SplitIntoWords(String), TEXT("-")).ToLower();
}
TArray<FString> UDirectiveUtilStringFunctionLibrary::SortStringArray(TArray<FString> StringArray)
{
Algo::Sort(StringArray);
return StringArray;
}
TArray<FString> UDirectiveUtilStringFunctionLibrary::GetSortedStringArray(const TArray<FString> StringArray)
{
TArray<FString> SortedArray = StringArray;
Algo::Sort(SortedArray);
return SortedArray;
}
int32 UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(const FString& A, const FString& B, const bool bCaseSensitive)
{
const FString StringA = bCaseSensitive ? A : A.ToLower();
const FString StringB = bCaseSensitive ? B : B.ToLower();
const int32 LenA = StringA.Len();
const int32 LenB = StringB.Len();
if (LenA == 0) { return LenB; }
if (LenB == 0) { return LenA; }
TArray<int32> PreviousRow;
TArray<int32> CurrentRow;
PreviousRow.SetNumUninitialized(LenB + 1);
CurrentRow.SetNumUninitialized(LenB + 1);
for (int32 ColumnIndex = 0; ColumnIndex <= LenB; ++ColumnIndex)
{
PreviousRow[ColumnIndex] = ColumnIndex;
}
for (int32 RowIndex = 1; RowIndex <= LenA; ++RowIndex)
{
CurrentRow[0] = RowIndex;
for (int32 ColumnIndex = 1; ColumnIndex <= LenB; ++ColumnIndex)
{
const int32 SubstitutionCost = (StringA[RowIndex - 1] == StringB[ColumnIndex - 1]) ? 0 : 1;
CurrentRow[ColumnIndex] = FMath::Min3(
PreviousRow[ColumnIndex] + 1,
CurrentRow[ColumnIndex - 1] + 1,
PreviousRow[ColumnIndex - 1] + SubstitutionCost);
}
Exchange(PreviousRow, CurrentRow);
}
return PreviousRow[LenB];
}
float UDirectiveUtilStringFunctionLibrary::GetStringSimilarity(const FString& A, const FString& B, const bool bCaseSensitive)
{
const int32 MaxLength = FMath::Max(A.Len(), B.Len());
if (MaxLength == 0)
{
return 1.0f;
}
const int32 Distance = GetLevenshteinDistance(A, B, bCaseSensitive);
return 1.0f - (static_cast<float>(Distance) / static_cast<float>(MaxLength));
}
bool UDirectiveUtilStringFunctionLibrary::ContainsAny(const FString& Source, const TArray<FString>& SearchTerms, const bool bCaseSensitive)
{
const ESearchCase::Type SearchCase = bCaseSensitive ? ESearchCase::CaseSensitive : ESearchCase::IgnoreCase;
for (const FString& Term : SearchTerms)
{
if (!Term.IsEmpty() && Source.Contains(Term, SearchCase))
{
return true;
}
}
return false;
}
bool UDirectiveUtilStringFunctionLibrary::FindFirstOfAny(const FString& Source, const TArray<FString>& SearchTerms, const bool bCaseSensitive, int32& OutFoundIndex, int32& OutTermIndex)
{
OutFoundIndex = INDEX_NONE;
OutTermIndex = INDEX_NONE;
const ESearchCase::Type SearchCase = bCaseSensitive ? ESearchCase::CaseSensitive : ESearchCase::IgnoreCase;
for (int32 TermIndex = 0; TermIndex < SearchTerms.Num(); ++TermIndex)
{
const FString& Term = SearchTerms[TermIndex];
if (Term.IsEmpty())
{
continue;
}
const int32 FoundIndex = Source.Find(Term, SearchCase, ESearchDir::FromStart);
if (FoundIndex != INDEX_NONE && (OutFoundIndex == INDEX_NONE || FoundIndex < OutFoundIndex))
{
OutFoundIndex = FoundIndex;
OutTermIndex = TermIndex;
}
}
return OutFoundIndex != INDEX_NONE;
}
FString UDirectiveUtilStringFunctionLibrary::Base64Encode(const FString& Source)
{
return FBase64::Encode(Source);
}
bool UDirectiveUtilStringFunctionLibrary::Base64Decode(const FString& Source, FString& OutDecoded)
{
OutDecoded.Reset();
return FBase64::Decode(Source, OutDecoded);
}
FString UDirectiveUtilStringFunctionLibrary::HexEncode(const FString& String)
{
return HexEncodeBytes(StringToUtf8Bytes(String));
}
bool UDirectiveUtilStringFunctionLibrary::HexDecode(const FString& Hex, FString& OutString)
{
OutString.Reset();
TArray<uint8> Bytes;
if (!HexDecodeBytes(Hex, Bytes))
{
return false;
}
const FUTF8ToTCHAR Converter(reinterpret_cast<const ANSICHAR*>(Bytes.GetData()), Bytes.Num());
OutString = FString(Converter.Length(), Converter.Get());
return true;
}
FString UDirectiveUtilStringFunctionLibrary::HexEncodeBytes(const TArray<uint8>& Bytes)
{
return BytesToHexLower(Bytes.GetData(), Bytes.Num());
}
bool UDirectiveUtilStringFunctionLibrary::HexDecodeBytes(const FString& Hex, TArray<uint8>& OutBytes)
{
OutBytes.Reset();
if (Hex.Len() % 2 != 0)
{
return false;
}
OutBytes.Reserve(Hex.Len() / 2);
for (int32 Index = 0; Index < Hex.Len(); Index += 2)
{
const TCHAR High = Hex[Index];
const TCHAR Low = Hex[Index + 1];
if (!CheckTCharIsHex(High) || !CheckTCharIsHex(Low))
{
OutBytes.Reset();
return false;
}
OutBytes.Add(static_cast<uint8>((TCharToNibble(High) << 4) | TCharToNibble(Low)));
}
return true;
}
FString UDirectiveUtilStringFunctionLibrary::Md5HashString(const FString& String)
{
return Md5HashBytes(StringToUtf8Bytes(String));
}
FString UDirectiveUtilStringFunctionLibrary::Md5HashBytes(const TArray<uint8>& Bytes)
{
return FMD5::HashBytes(Bytes.GetData(), Bytes.Num());
}
FString UDirectiveUtilStringFunctionLibrary::Sha1HashString(const FString& String)
{
return Sha1HashBytes(StringToUtf8Bytes(String));
}
FString UDirectiveUtilStringFunctionLibrary::Sha1HashBytes(const TArray<uint8>& Bytes)
{
uint8 Digest[20];
FSHA1::HashBuffer(Bytes.GetData(), Bytes.Num(), Digest);
return BytesToHexLower(Digest, UE_ARRAY_COUNT(Digest));
}
int32 UDirectiveUtilStringFunctionLibrary::Crc32String(const FString& String)
{
return Crc32Bytes(StringToUtf8Bytes(String));
}
int32 UDirectiveUtilStringFunctionLibrary::Crc32Bytes(const TArray<uint8>& Bytes)
{
return static_cast<int32>(FCrc::MemCrc32(Bytes.GetData(), Bytes.Num()));
}
bool UDirectiveUtilStringFunctionLibrary::IsValidFileName(const FString& String)
{
return !String.IsEmpty() && FPaths::GetCleanFilename(String) == String && SanitizeFileName(String) == String;
}
FString UDirectiveUtilStringFunctionLibrary::SanitizeFileName(const FString& String, const FString& Replacement)
{
return FPaths::MakeValidFileName(String, Replacement.IsEmpty() ? TEXT('\0') : Replacement[0]);
}
int32 UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(const FString& Input, const TArray<FString>& Candidates, float& OutSimilarity, const bool bCaseSensitive)
{
OutSimilarity = 0.0f;
int32 BestIndex = INDEX_NONE;
for (int32 Index = 0; Index < Candidates.Num(); ++Index)
{
const float Similarity = GetStringSimilarity(Input, Candidates[Index], bCaseSensitive);
if (BestIndex == INDEX_NONE || Similarity > OutSimilarity)
{
BestIndex = Index;
OutSimilarity = Similarity;
}
}
return BestIndex;
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilTextFunctionLibrary.h"
bool UDirectiveUtilTextFunctionLibrary::IsNotEmpty(const FText& Text)
{
return !Text.IsEmpty();
}

View File

@@ -0,0 +1,245 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Tasks/DirectiveUtilTask_AsyncLoadAsset.h"
#include "DirectiveUtilLogChannels.h"
#include "Engine/AssetManager.h"
#include "Engine/StreamableManager.h"
UDirectiveUtilTask_AsyncLoadAsset* UDirectiveUtilTask_AsyncLoadAsset::AsyncLoadAsset(UObject* WorldContextObject, const TSoftObjectPtr<UObject> Asset)
{
UDirectiveUtilTask_AsyncLoadAsset* Action = NewObject<UDirectiveUtilTask_AsyncLoadAsset>();
Action->SoftAsset = Asset;
if (WorldContextObject)
{
Action->RegisterWithGameInstance(WorldContextObject);
}
return Action;
}
void UDirectiveUtilTask_AsyncLoadAsset::Activate()
{
if (SoftAsset.IsNull())
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Async Load Asset failed to activate. The soft object reference is null."));
Failed.Broadcast(nullptr);
SetReadyToDestroy();
return;
}
if (!UAssetManager::GetIfInitialized())
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Async Load Asset failed to activate. The Asset Manager is not initialized."));
Failed.Broadcast(nullptr);
SetReadyToDestroy();
return;
}
StreamableHandle = UAssetManager::GetStreamableManager().RequestAsyncLoad(
SoftAsset.ToSoftObjectPath(),
FStreamableDelegate::CreateUObject(this, &UDirectiveUtilTask_AsyncLoadAsset::OnLoaded),
FStreamableManager::DefaultAsyncLoadPriority);
if (!StreamableHandle.IsValid())
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Async Load Asset failed to start the load request."));
Failed.Broadcast(nullptr);
SetReadyToDestroy();
}
}
void UDirectiveUtilTask_AsyncLoadAsset::OnLoaded()
{
UObject* LoadedAsset = StreamableHandle.IsValid() ? StreamableHandle->GetLoadedAsset() : nullptr;
if (LoadedAsset)
{
Completed.Broadcast(LoadedAsset);
}
else
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Async Load Asset completed but the asset could not be resolved."));
Failed.Broadcast(nullptr);
}
SetReadyToDestroy();
}
void UDirectiveUtilTask_AsyncLoadAsset::Cancel()
{
if (StreamableHandle.IsValid() && StreamableHandle->IsActive())
{
StreamableHandle->CancelHandle();
}
SetReadyToDestroy();
}
UDirectiveUtilTask_AsyncLoadClass* UDirectiveUtilTask_AsyncLoadClass::AsyncLoadClass(UObject* WorldContextObject, const TSoftClassPtr<UObject> AssetClass)
{
UDirectiveUtilTask_AsyncLoadClass* Action = NewObject<UDirectiveUtilTask_AsyncLoadClass>();
Action->SoftClass = AssetClass;
if (WorldContextObject)
{
Action->RegisterWithGameInstance(WorldContextObject);
}
return Action;
}
void UDirectiveUtilTask_AsyncLoadClass::Activate()
{
if (SoftClass.IsNull())
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Async Load Class failed to activate. The soft class reference is null."));
Failed.Broadcast(nullptr);
SetReadyToDestroy();
return;
}
if (!UAssetManager::GetIfInitialized())
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Async Load Class failed to activate. The Asset Manager is not initialized."));
Failed.Broadcast(nullptr);
SetReadyToDestroy();
return;
}
StreamableHandle = UAssetManager::GetStreamableManager().RequestAsyncLoad(
SoftClass.ToSoftObjectPath(),
FStreamableDelegate::CreateUObject(this, &UDirectiveUtilTask_AsyncLoadClass::OnLoaded),
FStreamableManager::DefaultAsyncLoadPriority);
if (!StreamableHandle.IsValid())
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Async Load Class failed to start the load request."));
Failed.Broadcast(nullptr);
SetReadyToDestroy();
}
}
void UDirectiveUtilTask_AsyncLoadClass::OnLoaded()
{
UClass* LoadedClass = StreamableHandle.IsValid() ? Cast<UClass>(StreamableHandle->GetLoadedAsset()) : nullptr;
if (LoadedClass)
{
Completed.Broadcast(LoadedClass);
}
else
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Async Load Class completed but the class could not be resolved."));
Failed.Broadcast(nullptr);
}
SetReadyToDestroy();
}
void UDirectiveUtilTask_AsyncLoadClass::Cancel()
{
if (StreamableHandle.IsValid() && StreamableHandle->IsActive())
{
StreamableHandle->CancelHandle();
}
SetReadyToDestroy();
}
UDirectiveUtilTask_AsyncLoadAssets* UDirectiveUtilTask_AsyncLoadAssets::AsyncLoadAssets(UObject* WorldContextObject, const TArray<TSoftObjectPtr<UObject>>& Assets)
{
UDirectiveUtilTask_AsyncLoadAssets* Action = NewObject<UDirectiveUtilTask_AsyncLoadAssets>();
Action->SoftAssets = Assets;
if (WorldContextObject)
{
Action->RegisterWithGameInstance(WorldContextObject);
}
return Action;
}
void UDirectiveUtilTask_AsyncLoadAssets::Activate()
{
// Unset references are filtered out of the request but keep their null slots in the output;
// duplicates are requested once and resolved per slot.
TArray<FSoftObjectPath> PathsToLoad;
for (const TSoftObjectPtr<UObject>& SoftAsset : SoftAssets)
{
if (!SoftAsset.IsNull())
{
PathsToLoad.AddUnique(SoftAsset.ToSoftObjectPath());
}
}
if (PathsToLoad.Num() == 0)
{
// Nothing to request; an empty request list is an error path in the streamable manager.
OnLoaded();
return;
}
if (!UAssetManager::GetIfInitialized())
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Async Load Assets failed to activate. The Asset Manager is not initialized."));
OnLoaded();
return;
}
StreamableHandle = UAssetManager::GetStreamableManager().RequestAsyncLoad(
MoveTemp(PathsToLoad),
FStreamableDelegate::CreateUObject(this, &UDirectiveUtilTask_AsyncLoadAssets::OnLoaded),
FStreamableManager::DefaultAsyncLoadPriority);
if (!StreamableHandle.IsValid())
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Async Load Assets failed to start the load request."));
OnLoaded();
return;
}
// Binding fails when the load already finished (all assets were in memory); complete directly,
// with the guard keeping the broadcast exactly once.
if (!StreamableHandle->BindUpdateDelegate(FStreamableUpdateDelegate::CreateUObject(this, &UDirectiveUtilTask_AsyncLoadAssets::OnUpdate)))
{
OnLoaded();
}
}
void UDirectiveUtilTask_AsyncLoadAssets::OnLoaded()
{
if (bHasCompleted)
{
return;
}
bHasCompleted = true;
TArray<UObject*> LoadedAssets;
LoadedAssets.Reserve(SoftAssets.Num());
for (const TSoftObjectPtr<UObject>& SoftAsset : SoftAssets)
{
LoadedAssets.Add(SoftAsset.Get());
}
Completed.Broadcast(LoadedAssets);
SetReadyToDestroy();
}
void UDirectiveUtilTask_AsyncLoadAssets::OnUpdate(TSharedRef<FStreamableHandle> Handle)
{
if (bHasCompleted)
{
return;
}
int32 LoadedCount = 0;
int32 RequestedCount = 0;
Handle->GetLoadedCount(LoadedCount, RequestedCount);
Progress.Broadcast(LoadedCount, RequestedCount);
}
void UDirectiveUtilTask_AsyncLoadAssets::Cancel()
{
bHasCompleted = true;
if (StreamableHandle.IsValid() && StreamableHandle->IsActive())
{
StreamableHandle->CancelHandle();
}
SetReadyToDestroy();
}

View File

@@ -0,0 +1,139 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Tasks/DirectiveUtilTask_AsyncTrace.h"
#include "DirectiveUtilLogChannels.h"
#include "Engine/Engine.h"
#include "Engine/World.h"
#include "WorldCollision.h"
#include "CollisionShape.h"
#include "CollisionQueryParams.h"
UDirectiveUtilTask_AsyncTrace* UDirectiveUtilTask_AsyncTrace::AsyncLineTraceByChannel(UObject* WorldContextObject, const FVector Start, const FVector End, const ETraceTypeQuery TraceChannel, const bool bMultiTrace)
{
UDirectiveUtilTask_AsyncTrace* Action = NewObject<UDirectiveUtilTask_AsyncTrace>();
Action->WorldContextObject = WorldContextObject;
Action->Start = Start;
Action->End = End;
Action->TraceChannel = TraceChannel;
Action->bMultiTrace = bMultiTrace;
Action->Shape = EDirectiveUtilTraceShape::Line;
if (WorldContextObject)
{
Action->RegisterWithGameInstance(WorldContextObject);
}
return Action;
}
UDirectiveUtilTask_AsyncTrace* UDirectiveUtilTask_AsyncTrace::AsyncSphereTraceByChannel(UObject* WorldContextObject, const FVector Start, const FVector End, const float Radius, const ETraceTypeQuery TraceChannel, const bool bMultiTrace)
{
UDirectiveUtilTask_AsyncTrace* Action = NewObject<UDirectiveUtilTask_AsyncTrace>();
Action->WorldContextObject = WorldContextObject;
Action->Start = Start;
Action->End = End;
Action->Radius = Radius;
Action->TraceChannel = TraceChannel;
Action->bMultiTrace = bMultiTrace;
Action->Shape = EDirectiveUtilTraceShape::Sphere;
if (WorldContextObject)
{
Action->RegisterWithGameInstance(WorldContextObject);
}
return Action;
}
UDirectiveUtilTask_AsyncTrace* UDirectiveUtilTask_AsyncTrace::AsyncBoxTraceByChannel(UObject* WorldContextObject, const FVector Start, const FVector End, const FVector HalfSize, const FRotator Orientation, const ETraceTypeQuery TraceChannel, const bool bMultiTrace)
{
UDirectiveUtilTask_AsyncTrace* Action = NewObject<UDirectiveUtilTask_AsyncTrace>();
Action->WorldContextObject = WorldContextObject;
Action->Start = Start;
Action->End = End;
Action->HalfSize = HalfSize;
Action->Orientation = Orientation.Quaternion();
Action->TraceChannel = TraceChannel;
Action->bMultiTrace = bMultiTrace;
Action->Shape = EDirectiveUtilTraceShape::Box;
if (WorldContextObject)
{
Action->RegisterWithGameInstance(WorldContextObject);
}
return Action;
}
UDirectiveUtilTask_AsyncTrace* UDirectiveUtilTask_AsyncTrace::AsyncCapsuleTraceByChannel(UObject* WorldContextObject, const FVector Start, const FVector End, const float Radius, const float HalfHeight, const ETraceTypeQuery TraceChannel, const bool bMultiTrace)
{
UDirectiveUtilTask_AsyncTrace* Action = NewObject<UDirectiveUtilTask_AsyncTrace>();
Action->WorldContextObject = WorldContextObject;
Action->Start = Start;
Action->End = End;
Action->Radius = Radius;
Action->HalfHeight = HalfHeight;
Action->TraceChannel = TraceChannel;
Action->bMultiTrace = bMultiTrace;
Action->Shape = EDirectiveUtilTraceShape::Capsule;
if (WorldContextObject)
{
Action->RegisterWithGameInstance(WorldContextObject);
}
return Action;
}
void UDirectiveUtilTask_AsyncTrace::Activate()
{
UWorld* World = WorldContextObject && GEngine
? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull)
: nullptr;
if (!World)
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Async Trace failed to activate. World is null."));
Completed.Broadcast(TArray<FHitResult>());
SetReadyToDestroy();
return;
}
const ECollisionChannel CollisionChannel = UEngineTypes::ConvertToCollisionChannel(TraceChannel);
const EAsyncTraceType AsyncType = bMultiTrace ? EAsyncTraceType::Multi : EAsyncTraceType::Single;
FTraceDelegate TraceDelegate;
TraceDelegate.BindUObject(this, &UDirectiveUtilTask_AsyncTrace::OnTraceComplete);
FCollisionQueryParams Params(FName(TEXT("DirectiveUtilAsyncTrace")), false);
if (Shape == EDirectiveUtilTraceShape::Line)
{
World->AsyncLineTraceByChannel(AsyncType, Start, End, CollisionChannel, Params, FCollisionResponseParams::DefaultResponseParam, &TraceDelegate);
}
else
{
FCollisionShape CollisionShape;
switch (Shape)
{
case EDirectiveUtilTraceShape::Sphere:
CollisionShape = FCollisionShape::MakeSphere(Radius);
break;
case EDirectiveUtilTraceShape::Box:
CollisionShape = FCollisionShape::MakeBox(HalfSize);
break;
case EDirectiveUtilTraceShape::Capsule:
CollisionShape = FCollisionShape::MakeCapsule(Radius, HalfHeight);
break;
default:
break;
}
World->AsyncSweepByChannel(AsyncType, Start, End, Orientation, CollisionChannel, CollisionShape, Params, FCollisionResponseParams::DefaultResponseParam, &TraceDelegate);
}
}
void UDirectiveUtilTask_AsyncTrace::OnTraceComplete(const FTraceHandle& Handle, FTraceDatum& Datum)
{
Completed.Broadcast(Datum.OutHits);
SetReadyToDestroy();
}

View File

@@ -0,0 +1,57 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Tasks/DirectiveUtilTask_Delay.h"
#include "DirectiveUtilLogChannels.h"
#include "Engine/Engine.h"
#include "Engine/World.h"
#include "TimerManager.h"
UDirectiveUtilTask_Delay* UDirectiveUtilTask_Delay::CancellableDelay(UObject* WorldContextObject, const float Duration)
{
UDirectiveUtilTask_Delay* Action = NewObject<UDirectiveUtilTask_Delay>();
Action->WorldContextObject = WorldContextObject;
Action->Duration = Duration;
if (WorldContextObject)
{
Action->RegisterWithGameInstance(WorldContextObject);
}
return Action;
}
void UDirectiveUtilTask_Delay::EndTask()
{
if (UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull) : nullptr)
{
World->GetTimerManager().ClearTimer(TimerHandle);
}
SetReadyToDestroy();
}
void UDirectiveUtilTask_Delay::Activate()
{
UWorld* World = WorldContextObject && GEngine
? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull)
: nullptr;
if (!World)
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Cancellable Delay failed to activate. World is null."));
SetReadyToDestroy();
return;
}
const float ClampedDuration = FMath::Max(Duration, KINDA_SMALL_NUMBER);
World->GetTimerManager().SetTimer(TimerHandle, this, &UDirectiveUtilTask_Delay::OnDelayComplete, ClampedDuration, false);
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Cancellable Delay started for %f seconds."), ClampedDuration);
}
void UDirectiveUtilTask_Delay::OnDelayComplete()
{
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Cancellable Delay completed."));
Completed.Broadcast();
SetReadyToDestroy();
}

View File

@@ -0,0 +1,280 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Tasks/DirectiveUtilTask_MoveToLocation.h"
#include "DirectiveUtilLogChannels.h"
#include "Blueprint/AIBlueprintHelperLibrary.h"
#include "Engine/World.h"
#include "DrawDebugHelpers.h"
#include "Navigation/PathFollowingComponent.h"
#include "TimerManager.h"
UDirectiveUtilTask_MoveToLocation* UDirectiveUtilTask_MoveToLocation::MoveToLocation(
UObject* WorldContextObject,
AController* Controller,
const FVector Destination,
const float AcceptanceRadius,
const bool bCheckStuckMovement,
const float StuckThreshold,
const bool bDebugLineTrace)
{
UDirectiveUtilTask_MoveToLocation* Action = NewObject<UDirectiveUtilTask_MoveToLocation>();
Action->Controller = Controller;
Action->Destination = Destination;
Action->AcceptanceRadius = AcceptanceRadius;
Action->bDebugLineTrace = bDebugLineTrace;
Action->StuckThreshold = StuckThreshold;
Action->bCheckStuckMovement = bCheckStuckMovement;
if (WorldContextObject)
{
Action->RegisterWithGameInstance(WorldContextObject);
}
return Action;
}
void UDirectiveUtilTask_MoveToLocation::EndTask()
{
ExecuteCompleted(false);
}
void UDirectiveUtilTask_MoveToLocation::Activate()
{
if(!Controller || !Controller->GetPawn())
{
ExecuteCompleted(false);
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller or pawn has been destroyed while moving to location. Aborting."));
return;
}
StartLocation = Controller->GetPawn()->GetActorLocation();
LastCheckedLocation = StartLocation;
CurrentLocation = StartLocation;
Controller->GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &UDirectiveUtilTask_MoveToLocation::CheckMoveToLocation, 0.1f, true);
if (bCheckStuckMovement)
{
Controller->GetWorld()->GetTimerManager().SetTimer(StuckTimerHandle, this, &UDirectiveUtilTask_MoveToLocation::CheckStuckMovement, 3.f, true);
}
UAIBlueprintHelperLibrary::SimpleMoveToLocation(Controller, Destination);
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Moving controller to location (%s)."), *Destination.ToString());
if (bDebugLineTrace)
{
DrawDebugLine(
Controller->GetWorld(),
Destination + FVector(0, 0, 100),
Destination,
FColor::Green,
false,
5.0f,
0,
1.0f
);
}
}
void UDirectiveUtilTask_MoveToLocation::CheckMoveToLocation()
{
if(!Controller || !Controller->GetPawn())
{
ExecuteCompleted(false);
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller or pawn has been destroyed while moving to location. Aborting."));
return;
}
CurrentLocation = Controller->GetPawn()->GetActorLocation();
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Controller is moving to location (%s). Current distance: %f."), *Destination.ToString(), FVector::Dist(CurrentLocation, Destination));
if (FVector::Dist(CurrentLocation, Destination) < AcceptanceRadius)
{
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Controller has moved to location."));
ExecuteCompleted(true);
return;
}
const UPathFollowingComponent* PathFollowing = Controller->FindComponentByClass<UPathFollowingComponent>();
if (!PathFollowing || PathFollowing->GetStatus() == EPathFollowingStatus::Idle)
{
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Path following has stopped. Completing move to location."));
ExecuteCompleted(FVector::Dist(CurrentLocation, Destination) < AcceptanceRadius);
}
}
void UDirectiveUtilTask_MoveToLocation::CheckStuckMovement()
{
if(!Controller || !Controller->GetPawn())
{
ExecuteCompleted(false);
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller or pawn has been destroyed while moving to location. Aborting."));
return;
}
if (FVector::Dist(CurrentLocation, LastCheckedLocation) < StuckThreshold)
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller is stuck while moving to location. Aborting"));
ExecuteCompleted(false);
}
LastCheckedLocation = CurrentLocation;
}
void UDirectiveUtilTask_MoveToLocation::ExecuteCompleted(const bool bSuccess)
{
if (bHasCompleted)
{
return;
}
bHasCompleted = true;
UE_LOG(LogDirectiveUtil, Log, TEXT("Movement to location completed. Success: %s."), bSuccess ? TEXT("true") : TEXT("false"));
if (Controller)
{
Controller->GetWorld()->GetTimerManager().ClearTimer(TimerHandle);
Controller->GetWorld()->GetTimerManager().ClearTimer(StuckTimerHandle);
}
Completed.Broadcast(bSuccess);
Controller = nullptr;
Destination = FVector::ZeroVector;
SetReadyToDestroy();
}
UDirectiveUtilTask_MoveToActor* UDirectiveUtilTask_MoveToActor::MoveToActor(
UObject* WorldContextObject,
AController* Controller,
AActor* Goal,
const float AcceptanceRadius,
const bool bCheckStuckMovement,
const float StuckThreshold)
{
UDirectiveUtilTask_MoveToActor* Action = NewObject<UDirectiveUtilTask_MoveToActor>();
Action->Controller = Controller;
Action->Goal = Goal;
Action->AcceptanceRadius = AcceptanceRadius;
Action->StuckThreshold = StuckThreshold;
Action->bCheckStuckMovement = bCheckStuckMovement;
if (WorldContextObject)
{
Action->RegisterWithGameInstance(WorldContextObject);
}
return Action;
}
void UDirectiveUtilTask_MoveToActor::EndTask()
{
ExecuteCompleted(false);
}
void UDirectiveUtilTask_MoveToActor::Activate()
{
if(!Controller || !Controller->GetPawn() || !IsValid(Goal))
{
ExecuteCompleted(false);
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller, pawn, or goal has been destroyed while moving to actor. Aborting."));
return;
}
StartLocation = Controller->GetPawn()->GetActorLocation();
LastCheckedLocation = StartLocation;
CurrentLocation = StartLocation;
Controller->GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &UDirectiveUtilTask_MoveToActor::CheckMoveToActor, 0.1f, true);
if (bCheckStuckMovement)
{
Controller->GetWorld()->GetTimerManager().SetTimer(StuckTimerHandle, this, &UDirectiveUtilTask_MoveToActor::CheckStuckMovement, 3.f, true);
}
UAIBlueprintHelperLibrary::SimpleMoveToActor(Controller, Goal);
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Moving controller to actor (%s)."), *GetNameSafe(Goal));
}
void UDirectiveUtilTask_MoveToActor::CheckMoveToActor()
{
if(!Controller || !Controller->GetPawn())
{
ExecuteCompleted(false);
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller or pawn has been destroyed while moving to actor. Aborting."));
return;
}
if (!IsValid(Goal))
{
ExecuteCompleted(false);
UE_LOG(LogDirectiveUtil, Warning, TEXT("Goal actor has been destroyed while moving to actor. Aborting."));
return;
}
// The goal can move, so its location is re-read every poll.
const FVector GoalLocation = Goal->GetActorLocation();
CurrentLocation = Controller->GetPawn()->GetActorLocation();
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Controller is moving to actor (%s). Current distance: %f."), *GetNameSafe(Goal), FVector::Dist(CurrentLocation, GoalLocation));
if (FVector::Dist(CurrentLocation, GoalLocation) < AcceptanceRadius)
{
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Controller has moved to actor."));
ExecuteCompleted(true);
return;
}
const UPathFollowingComponent* PathFollowing = Controller->FindComponentByClass<UPathFollowingComponent>();
if (!PathFollowing || PathFollowing->GetStatus() == EPathFollowingStatus::Idle)
{
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Path following has stopped. Completing move to actor."));
ExecuteCompleted(FVector::Dist(CurrentLocation, GoalLocation) < AcceptanceRadius);
}
}
void UDirectiveUtilTask_MoveToActor::CheckStuckMovement()
{
if(!Controller || !Controller->GetPawn())
{
ExecuteCompleted(false);
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller or pawn has been destroyed while moving to actor. Aborting."));
return;
}
if (FVector::Dist(CurrentLocation, LastCheckedLocation) < StuckThreshold)
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller is stuck while moving to actor. Aborting"));
ExecuteCompleted(false);
}
LastCheckedLocation = CurrentLocation;
}
void UDirectiveUtilTask_MoveToActor::ExecuteCompleted(const bool bSuccess)
{
if (bHasCompleted)
{
return;
}
bHasCompleted = true;
UE_LOG(LogDirectiveUtil, Log, TEXT("Movement to actor completed. Success: %s."), bSuccess ? TEXT("true") : TEXT("false"));
if (Controller)
{
Controller->GetWorld()->GetTimerManager().ClearTimer(TimerHandle);
Controller->GetWorld()->GetTimerManager().ClearTimer(StuckTimerHandle);
}
Completed.Broadcast(bSuccess);
Controller = nullptr;
Goal = nullptr;
SetReadyToDestroy();
}

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "Logging/LogMacros.h"
DIRECTIVEUTILITIESRUNTIME_API DECLARE_LOG_CATEGORY_EXTERN(LogDirectiveUtil, Log, All);
DIRECTIVEUTILITIESRUNTIME_API DECLARE_LOG_CATEGORY_EXTERN(LogDirectiveUtilEditor, Log, All);

View File

@@ -0,0 +1,15 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
class FDirectiveUtilitiesRuntimeModule : public IModuleInterface
{
public:
/** IModuleInterface implementation */
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};

View File

@@ -0,0 +1,716 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Net/Core/PushModel/PushModel.h"
#include "DirectiveUtilArrayFunctionLibrary.generated.h"
/**
* UDirectiveUtilArrayFunctionLibrary
* A collection of array utility functions that improve the usability of arrays in Blueprints.
*/
UCLASS()
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilArrayFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Returns the next index in the array.
* If the next index is greater than the last array index and bLoop is enabled, the index will loop back to the start of the array.
* Otherwise, the last index will be returned.
* Returns INDEX_NONE for an empty array.
* @param TargetArray - The array to get the next index for.
* @param Index - The current index.
* @param bLoop - If true, the index will loop back to the beginning of the array when the next index is greater than the last array index.
* Otherwise, the last index will be returned.
* @returns The next index in the array.
*/
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Next Index", CompactNodeTitle = "NEXT INDEX", ArrayParm = "TargetArray", BlueprintThreadSafe), Category="Directive Utilities|Array")
static int32 Array_NextIndex(const TArray<int32>& TargetArray, const int32 Index, const bool bLoop);
/**
* Returns the previous index in the array.
* If the previous index is less than 0 and bLoop is enabled, the index will loop back to the end of the array.
* Otherwise, 0 will be returned.
* Returns INDEX_NONE for an empty array.
* @param TargetArray - The array to get the previous index for.
* @param Index - The current index.
* @param bLoop - If the next index is greater than the last array index and bLoop is enabled, the index will loop back to the start of the array.
* Otherwise, the last index will be returned.
* @returns The previous index in the array.
*/
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Previous Index", CompactNodeTitle = "PREV INDEX", ArrayParm = "TargetArray", BlueprintThreadSafe), Category="Directive Utilities|Array")
static int32 Array_PreviousIndex(const TArray<int32>& TargetArray, const int32 Index, const bool bLoop);
/**
* Removes duplicate elements from the array in-place.
* @param TargetArray - The array to remove duplicates from.
*/
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Remove Duplicates", CompactNodeTitle = "REMOVE DUPLICATES", ArrayParm = "TargetArray"), Category="Directive Utilities|Array")
static void Array_RemoveDuplicates(const TArray<int32>& TargetArray);
/**
* Returns a copy of the first element of the array.
* @param TargetArray - The array to read from.
* @param OutItem - [out] A copy of the first element, or the default value if the array is empty.
* @returns True if the array contained a valid first element.
*/
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Get Valid First Array Item (Copy)", ArrayParm = "TargetArray", ArrayTypeDependentParams = "OutItem", BlueprintThreadSafe), Category="Directive Utilities|Array")
static bool Array_GetValidFirstItemCopy(const TArray<int32>& TargetArray, int32& OutItem);
/**
* Returns a copy of the last element of the array.
* @param TargetArray - The array to read from.
* @param OutItem - [out] A copy of the last element, or the default value if the array is empty.
* @returns True if the array contained a valid last element.
*/
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Get Valid Last Array Item (Copy)", ArrayParm = "TargetArray", ArrayTypeDependentParams = "OutItem", BlueprintThreadSafe), Category="Directive Utilities|Array")
static bool Array_GetValidLastItemCopy(const TArray<int32>& TargetArray, int32& OutItem);
/**
* Returns a copy of the element at the given index, if the index is valid.
* @param TargetArray - The array to read from.
* @param Index - The index to read.
* @param OutItem - [out] A copy of the element, or the default value if the index is invalid.
* @returns True if the index was valid.
*/
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Get Valid Array Item From Index (Copy)", ArrayParm = "TargetArray", ArrayTypeDependentParams = "OutItem", BlueprintThreadSafe), Category="Directive Utilities|Array")
static bool Array_GetValidItemFromIndexCopy(const TArray<int32>& TargetArray, const int32 Index, int32& OutItem);
/**
* Returns a copy of a random element from the array.
* @param TargetArray - The array to read from.
* @param OutItem - [out] A copy of the randomly selected element, or the default value if the array is empty.
* @param OutIndex - [out] The index of the selected element, or INDEX_NONE if the array is empty.
* @returns True if a valid element was selected.
*/
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Get Random Valid Array Item", CompactNodeTitle = "RANDOM", ArrayParm = "TargetArray", ArrayTypeDependentParams = "OutItem"), Category="Directive Utilities|Array")
static bool Array_GetRandomItem(const TArray<int32>& TargetArray, int32& OutItem, int32& OutIndex);
/**
* Returns a copy of the last element of the array without removing it.
* @param TargetArray - The array to read from.
* @param OutItem - [out] A copy of the last element, or the default value if the array is empty.
*/
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Last Value", CompactNodeTitle = "LAST", ArrayParm = "TargetArray", ArrayTypeDependentParams = "OutItem", BlueprintThreadSafe), Category="Directive Utilities|Array")
static void Array_LastValue(const TArray<int32>& TargetArray, int32& OutItem);
/**
* Removes the last element of the array and returns a copy of it.
* @param TargetArray - The array to pop from.
* @param OutItem - [out] A copy of the removed element, or the default value if the array is empty.
* @returns True if an element was removed.
*/
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Pop", ArrayParm = "TargetArray", ArrayTypeDependentParams = "OutItem"), Category="Directive Utilities|Array")
static bool Array_Pop(const TArray<int32>& TargetArray, int32& OutItem);
/**
* Removes the first element of the array and returns a copy of it.
* @param TargetArray - The array to pop from.
* @param OutItem - [out] A copy of the removed element, or the default value if the array is empty.
* @returns True if an element was removed.
*/
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Pop First", ArrayParm = "TargetArray", ArrayTypeDependentParams = "OutItem"), Category="Directive Utilities|Array")
static bool Array_PopFirst(const TArray<int32>& TargetArray, int32& OutItem);
/**
* Removes the element at the given index by swapping it with the last element (does not preserve order).
* This is O(1) but changes the position of the previously-last element.
* @param TargetArray - The array to remove from.
* @param Index - The index to remove.
* @returns True if the index was valid and an element was removed.
*/
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Remove At Swap", ArrayParm = "TargetArray"), Category="Directive Utilities|Array")
static bool Array_RemoveAtSwap(const TArray<int32>& TargetArray, const int32 Index);
/**
* Returns a copy of a contiguous range of the array. The range is clamped to the array bounds.
* @param TargetArray - The array to slice.
* @param StartIndex - The index to start copying from (clamped to [0, Length]).
* @param Count - The number of elements to copy. Values <= 0 produce an empty array.
* @param OutArray - [out] The sliced copy.
*/
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Slice", ArrayParm = "TargetArray,OutArray", ArrayTypeDependentParams = "OutArray", BlueprintThreadSafe), Category="Directive Utilities|Array")
static void Array_Slice(const TArray<int32>& TargetArray, const int32 StartIndex, const int32 Count, TArray<int32>& OutArray);
/**
* Cyclically rotates the elements of the array in place.
* @param TargetArray - The array to rotate.
* @param Shift - The number of positions to rotate. Positive rotates toward the end; negative toward the start.
*/
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Rotate", ArrayParm = "TargetArray"), Category="Directive Utilities|Array")
static void Array_Rotate(const TArray<int32>& TargetArray, const int32 Shift);
/**
* Returns a copy of the array with duplicates removed, keeping the first occurrence and preserving order.
* Unlike Remove Duplicates, this does not modify the input array.
* @param TargetArray - The array to read from.
* @param OutArray - [out] The de-duplicated copy.
*/
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Get Distinct (Copy)", ArrayParm = "TargetArray,OutArray", ArrayTypeDependentParams = "OutArray", BlueprintThreadSafe), Category="Directive Utilities|Array")
static void Array_GetDistinct(const TArray<int32>& TargetArray, TArray<int32>& OutArray);
/**
* Counts how many times an item appears in the array.
* @param TargetArray - The array to search.
* @param ItemToCount - The item to count.
* @returns The number of occurrences.
*/
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Count Occurrences", ArrayParm = "TargetArray", ArrayTypeDependentParams = "ItemToCount", AutoCreateRefTerm = "ItemToCount", BlueprintThreadSafe), Category="Directive Utilities|Array")
static int32 Array_CountOccurrences(const TArray<int32>& TargetArray, const int32& ItemToCount);
/**
* Returns the most frequently occurring element of the array (ties resolve to the earliest such element).
* @param TargetArray - The array to read from.
* @param OutItem - [out] A copy of the most common element, or the default value if the array is empty.
* @param OutCount - [out] The number of times the most common element occurs.
* @returns True if the array was non-empty.
*/
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Get Most Common", ArrayParm = "TargetArray", ArrayTypeDependentParams = "OutItem", BlueprintThreadSafe), Category="Directive Utilities|Array")
static bool Array_GetMostCommon(const TArray<int32>& TargetArray, int32& OutItem, int32& OutCount);
/*~
* Native functions that will be called by the below custom thunk layers, which read off the property address and call the appropriate native handler.
* Based off UKismetArrayLibrary implementation
~*/
static int32 GenericArray_NextIndex(const void* TargetArray, const FArrayProperty* ArrayProperty, int32 Index, bool bLoop);
static int32 GenericArray_PreviousIndex(const void* TargetArray, const FArrayProperty* ArrayProperty, int32 Index, bool bLoop);
static void GenericArray_RemoveDuplicates(void* TargetArray, const FArrayProperty* ArrayProperty);
static bool GenericArray_GetItemAtIndex(const void* TargetArray, const FArrayProperty* ArrayProperty, int32 Index, void* OutItemPtr);
static bool GenericArray_GetFirstItem(const void* TargetArray, const FArrayProperty* ArrayProperty, void* OutItemPtr);
static bool GenericArray_GetLastItem(const void* TargetArray, const FArrayProperty* ArrayProperty, void* OutItemPtr);
static bool GenericArray_GetRandomItem(const void* TargetArray, const FArrayProperty* ArrayProperty, void* OutItemPtr, int32* OutIndex);
static bool GenericArray_Pop(void* TargetArray, const FArrayProperty* ArrayProperty, void* OutItemPtr);
static bool GenericArray_PopFirst(void* TargetArray, const FArrayProperty* ArrayProperty, void* OutItemPtr);
static bool GenericArray_RemoveAtSwap(void* TargetArray, const FArrayProperty* ArrayProperty, int32 Index);
static void GenericArray_Slice(const void* TargetArray, const FArrayProperty* TargetArrayProperty, int32 StartIndex, int32 Count, void* OutArray, const FArrayProperty* OutArrayProperty);
static void GenericArray_Rotate(void* TargetArray, const FArrayProperty* ArrayProperty, int32 Shift);
static void GenericArray_GetDistinct(const void* TargetArray, const FArrayProperty* TargetArrayProperty, void* OutArray, const FArrayProperty* OutArrayProperty);
static int32 GenericArray_CountOccurrences(const void* TargetArray, const FArrayProperty* ArrayProperty, const void* ItemToCount);
static bool GenericArray_GetMostCommon(const void* TargetArray, const FArrayProperty* ArrayProperty, void* OutItemPtr, int32* OutCount);
/*~
* Custom thunk layers that read off the property address and call the appropriate native handler.
* Based off UKismetArrayLibrary implementation
~*/
DECLARE_FUNCTION(execArray_NextIndex)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
const void* ArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
P_GET_PROPERTY(FIntProperty, Index);
P_GET_UBOOL(bLoop);
P_FINISH;
P_NATIVE_BEGIN;
*static_cast<int32*>(RESULT_PARAM) = GenericArray_NextIndex(ArrayAddr, ArrayProperty, Index, bLoop);
P_NATIVE_END;
}
DECLARE_FUNCTION(execArray_PreviousIndex)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
const void* ArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
P_GET_PROPERTY(FIntProperty, Index);
P_GET_UBOOL(bLoop);
P_FINISH;
P_NATIVE_BEGIN;
*static_cast<int32*>(RESULT_PARAM) = GenericArray_PreviousIndex(ArrayAddr, ArrayProperty, Index, bLoop);
P_NATIVE_END;
}
DECLARE_FUNCTION(execArray_RemoveDuplicates)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
void* ArrayAddr = Stack.MostRecentPropertyAddress;
FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
P_FINISH;
P_NATIVE_BEGIN;
MARK_PROPERTY_DIRTY(Stack.Object, ArrayProperty);
GenericArray_RemoveDuplicates(ArrayAddr, ArrayProperty);
P_NATIVE_END;
}
DECLARE_FUNCTION(execArray_GetValidFirstItemCopy)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
const void* ArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
const FProperty* InnerProp = ArrayProperty->Inner;
const int32 PropertySize = InnerProp->GetElementSize() * InnerProp->ArrayDim;
void* StorageSpace = FMemory_Alloca(PropertySize);
InnerProp->InitializeValue(StorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(StorageSpace);
void* ItemPtr;
if (Stack.MostRecentPropertyAddress != nullptr && Stack.MostRecentProperty != nullptr
&& PropertySize == Stack.MostRecentProperty->GetElementSize() * Stack.MostRecentProperty->ArrayDim
&& (Stack.MostRecentProperty->GetClass()->IsChildOf(InnerProp->GetClass())
|| InnerProp->GetClass()->IsChildOf(Stack.MostRecentProperty->GetClass())))
{
ItemPtr = Stack.MostRecentPropertyAddress;
}
else
{
ItemPtr = StorageSpace;
}
P_FINISH;
P_NATIVE_BEGIN;
*static_cast<bool*>(RESULT_PARAM) = GenericArray_GetFirstItem(ArrayAddr, ArrayProperty, ItemPtr);
P_NATIVE_END;
InnerProp->DestroyValue(StorageSpace);
}
DECLARE_FUNCTION(execArray_GetValidLastItemCopy)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
const void* ArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
const FProperty* InnerProp = ArrayProperty->Inner;
const int32 PropertySize = InnerProp->GetElementSize() * InnerProp->ArrayDim;
void* StorageSpace = FMemory_Alloca(PropertySize);
InnerProp->InitializeValue(StorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(StorageSpace);
void* ItemPtr;
if (Stack.MostRecentPropertyAddress != nullptr && Stack.MostRecentProperty != nullptr
&& PropertySize == Stack.MostRecentProperty->GetElementSize() * Stack.MostRecentProperty->ArrayDim
&& (Stack.MostRecentProperty->GetClass()->IsChildOf(InnerProp->GetClass())
|| InnerProp->GetClass()->IsChildOf(Stack.MostRecentProperty->GetClass())))
{
ItemPtr = Stack.MostRecentPropertyAddress;
}
else
{
ItemPtr = StorageSpace;
}
P_FINISH;
P_NATIVE_BEGIN;
*static_cast<bool*>(RESULT_PARAM) = GenericArray_GetLastItem(ArrayAddr, ArrayProperty, ItemPtr);
P_NATIVE_END;
InnerProp->DestroyValue(StorageSpace);
}
DECLARE_FUNCTION(execArray_GetValidItemFromIndexCopy)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
const void* ArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
P_GET_PROPERTY(FIntProperty, Index);
const FProperty* InnerProp = ArrayProperty->Inner;
const int32 PropertySize = InnerProp->GetElementSize() * InnerProp->ArrayDim;
void* StorageSpace = FMemory_Alloca(PropertySize);
InnerProp->InitializeValue(StorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(StorageSpace);
void* ItemPtr;
if (Stack.MostRecentPropertyAddress != nullptr && Stack.MostRecentProperty != nullptr
&& PropertySize == Stack.MostRecentProperty->GetElementSize() * Stack.MostRecentProperty->ArrayDim
&& (Stack.MostRecentProperty->GetClass()->IsChildOf(InnerProp->GetClass())
|| InnerProp->GetClass()->IsChildOf(Stack.MostRecentProperty->GetClass())))
{
ItemPtr = Stack.MostRecentPropertyAddress;
}
else
{
ItemPtr = StorageSpace;
}
P_FINISH;
P_NATIVE_BEGIN;
*static_cast<bool*>(RESULT_PARAM) = GenericArray_GetItemAtIndex(ArrayAddr, ArrayProperty, Index, ItemPtr);
P_NATIVE_END;
InnerProp->DestroyValue(StorageSpace);
}
DECLARE_FUNCTION(execArray_GetRandomItem)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
const void* ArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
const FProperty* InnerProp = ArrayProperty->Inner;
const int32 PropertySize = InnerProp->GetElementSize() * InnerProp->ArrayDim;
void* StorageSpace = FMemory_Alloca(PropertySize);
InnerProp->InitializeValue(StorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(StorageSpace);
void* ItemPtr;
if (Stack.MostRecentPropertyAddress != nullptr && Stack.MostRecentProperty != nullptr
&& PropertySize == Stack.MostRecentProperty->GetElementSize() * Stack.MostRecentProperty->ArrayDim
&& (Stack.MostRecentProperty->GetClass()->IsChildOf(InnerProp->GetClass())
|| InnerProp->GetClass()->IsChildOf(Stack.MostRecentProperty->GetClass())))
{
ItemPtr = Stack.MostRecentPropertyAddress;
}
else
{
ItemPtr = StorageSpace;
}
Stack.MostRecentProperty = nullptr;
Stack.MostRecentPropertyAddress = nullptr;
Stack.StepCompiledIn<FProperty>(nullptr);
int32* OutIndex = reinterpret_cast<int32*>(Stack.MostRecentPropertyAddress);
P_FINISH;
P_NATIVE_BEGIN;
*static_cast<bool*>(RESULT_PARAM) = GenericArray_GetRandomItem(ArrayAddr, ArrayProperty, ItemPtr, OutIndex);
P_NATIVE_END;
InnerProp->DestroyValue(StorageSpace);
}
DECLARE_FUNCTION(execArray_LastValue)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
const void* ArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
const FProperty* InnerProp = ArrayProperty->Inner;
const int32 PropertySize = InnerProp->GetElementSize() * InnerProp->ArrayDim;
void* StorageSpace = FMemory_Alloca(PropertySize);
InnerProp->InitializeValue(StorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(StorageSpace);
void* ItemPtr;
if (Stack.MostRecentPropertyAddress != nullptr && Stack.MostRecentProperty != nullptr
&& PropertySize == Stack.MostRecentProperty->GetElementSize() * Stack.MostRecentProperty->ArrayDim
&& (Stack.MostRecentProperty->GetClass()->IsChildOf(InnerProp->GetClass())
|| InnerProp->GetClass()->IsChildOf(Stack.MostRecentProperty->GetClass())))
{
ItemPtr = Stack.MostRecentPropertyAddress;
}
else
{
ItemPtr = StorageSpace;
}
P_FINISH;
P_NATIVE_BEGIN;
GenericArray_GetLastItem(ArrayAddr, ArrayProperty, ItemPtr);
P_NATIVE_END;
InnerProp->DestroyValue(StorageSpace);
}
DECLARE_FUNCTION(execArray_Pop)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
void* ArrayAddr = Stack.MostRecentPropertyAddress;
FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
const FProperty* InnerProp = ArrayProperty->Inner;
const int32 PropertySize = InnerProp->GetElementSize() * InnerProp->ArrayDim;
void* StorageSpace = FMemory_Alloca(PropertySize);
InnerProp->InitializeValue(StorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(StorageSpace);
void* ItemPtr;
if (Stack.MostRecentPropertyAddress != nullptr && Stack.MostRecentProperty != nullptr
&& PropertySize == Stack.MostRecentProperty->GetElementSize() * Stack.MostRecentProperty->ArrayDim
&& (Stack.MostRecentProperty->GetClass()->IsChildOf(InnerProp->GetClass())
|| InnerProp->GetClass()->IsChildOf(Stack.MostRecentProperty->GetClass())))
{
ItemPtr = Stack.MostRecentPropertyAddress;
}
else
{
ItemPtr = StorageSpace;
}
P_FINISH;
P_NATIVE_BEGIN;
MARK_PROPERTY_DIRTY(Stack.Object, ArrayProperty);
*static_cast<bool*>(RESULT_PARAM) = GenericArray_Pop(ArrayAddr, ArrayProperty, ItemPtr);
P_NATIVE_END;
InnerProp->DestroyValue(StorageSpace);
}
DECLARE_FUNCTION(execArray_PopFirst)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
void* ArrayAddr = Stack.MostRecentPropertyAddress;
FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
const FProperty* InnerProp = ArrayProperty->Inner;
const int32 PropertySize = InnerProp->GetElementSize() * InnerProp->ArrayDim;
void* StorageSpace = FMemory_Alloca(PropertySize);
InnerProp->InitializeValue(StorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(StorageSpace);
void* ItemPtr;
if (Stack.MostRecentPropertyAddress != nullptr && Stack.MostRecentProperty != nullptr
&& PropertySize == Stack.MostRecentProperty->GetElementSize() * Stack.MostRecentProperty->ArrayDim
&& (Stack.MostRecentProperty->GetClass()->IsChildOf(InnerProp->GetClass())
|| InnerProp->GetClass()->IsChildOf(Stack.MostRecentProperty->GetClass())))
{
ItemPtr = Stack.MostRecentPropertyAddress;
}
else
{
ItemPtr = StorageSpace;
}
P_FINISH;
P_NATIVE_BEGIN;
MARK_PROPERTY_DIRTY(Stack.Object, ArrayProperty);
*static_cast<bool*>(RESULT_PARAM) = GenericArray_PopFirst(ArrayAddr, ArrayProperty, ItemPtr);
P_NATIVE_END;
InnerProp->DestroyValue(StorageSpace);
}
DECLARE_FUNCTION(execArray_RemoveAtSwap)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
void* ArrayAddr = Stack.MostRecentPropertyAddress;
FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
P_GET_PROPERTY(FIntProperty, Index);
P_FINISH;
P_NATIVE_BEGIN;
MARK_PROPERTY_DIRTY(Stack.Object, ArrayProperty);
*static_cast<bool*>(RESULT_PARAM) = GenericArray_RemoveAtSwap(ArrayAddr, ArrayProperty, Index);
P_NATIVE_END;
}
DECLARE_FUNCTION(execArray_Slice)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
const void* SourceArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* SourceArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!SourceArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
P_GET_PROPERTY(FIntProperty, StartIndex);
P_GET_PROPERTY(FIntProperty, Count);
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
void* OutArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* OutArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!OutArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
P_FINISH;
P_NATIVE_BEGIN;
GenericArray_Slice(SourceArrayAddr, SourceArrayProperty, StartIndex, Count, OutArrayAddr, OutArrayProperty);
P_NATIVE_END;
}
DECLARE_FUNCTION(execArray_Rotate)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
void* ArrayAddr = Stack.MostRecentPropertyAddress;
FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
P_GET_PROPERTY(FIntProperty, Shift);
P_FINISH;
P_NATIVE_BEGIN;
MARK_PROPERTY_DIRTY(Stack.Object, ArrayProperty);
GenericArray_Rotate(ArrayAddr, ArrayProperty, Shift);
P_NATIVE_END;
}
DECLARE_FUNCTION(execArray_GetDistinct)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
const void* SourceArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* SourceArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!SourceArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
void* OutArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* OutArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!OutArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
P_FINISH;
P_NATIVE_BEGIN;
GenericArray_GetDistinct(SourceArrayAddr, SourceArrayProperty, OutArrayAddr, OutArrayProperty);
P_NATIVE_END;
}
DECLARE_FUNCTION(execArray_CountOccurrences)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
const void* ArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
const FProperty* InnerProp = ArrayProperty->Inner;
const int32 PropertySize = InnerProp->GetElementSize() * InnerProp->ArrayDim;
void* StorageSpace = FMemory_Alloca(PropertySize);
InnerProp->InitializeValue(StorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(StorageSpace);
P_FINISH;
P_NATIVE_BEGIN;
*static_cast<int32*>(RESULT_PARAM) = GenericArray_CountOccurrences(ArrayAddr, ArrayProperty, StorageSpace);
P_NATIVE_END;
InnerProp->DestroyValue(StorageSpace);
}
DECLARE_FUNCTION(execArray_GetMostCommon)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
const void* ArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
const FProperty* InnerProp = ArrayProperty->Inner;
const int32 PropertySize = InnerProp->GetElementSize() * InnerProp->ArrayDim;
void* StorageSpace = FMemory_Alloca(PropertySize);
InnerProp->InitializeValue(StorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(StorageSpace);
void* ItemPtr;
if (Stack.MostRecentPropertyAddress != nullptr && Stack.MostRecentProperty != nullptr
&& PropertySize == Stack.MostRecentProperty->GetElementSize() * Stack.MostRecentProperty->ArrayDim
&& (Stack.MostRecentProperty->GetClass()->IsChildOf(InnerProp->GetClass())
|| InnerProp->GetClass()->IsChildOf(Stack.MostRecentProperty->GetClass())))
{
ItemPtr = Stack.MostRecentPropertyAddress;
}
else
{
ItemPtr = StorageSpace;
}
Stack.MostRecentProperty = nullptr;
Stack.MostRecentPropertyAddress = nullptr;
Stack.StepCompiledIn<FProperty>(nullptr);
int32* OutCount = reinterpret_cast<int32*>(Stack.MostRecentPropertyAddress);
P_FINISH;
P_NATIVE_BEGIN;
*static_cast<bool*>(RESULT_PARAM) = GenericArray_GetMostCommon(ArrayAddr, ArrayProperty, ItemPtr, OutCount);
P_NATIVE_END;
InnerProp->DestroyValue(StorageSpace);
}
};

View File

@@ -0,0 +1,104 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "DirectiveUtilFunctionLibrary.generated.h"
/**
* UDirectiveUtilFunctionLibrary
*
* The primary function library for the Directive Utilities plugin.
*/
UCLASS()
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Returns a list of classes derived from the given base class (not limited to Actors).
* This exposes the built-in GetDerivedClasses function to blueprints.
* @param BaseClass The base class to get the derived classes for.
* @param bRecursive Whether to include derived classes of derived classes.
* @param DerivedClasses The list of derived classes.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Utility")
static void GetChildClasses(const UClass* BaseClass, bool bRecursive, TArray<UClass*>& DerivedClasses);
/**
* Copy the provided text to the clipboard.
* @param Text - The text to copy to the clipboard.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Clipboard" )
static void CopyTextToClipboard(const FText& Text);
/**
* Copy the provided string to the clipboard.
* @param String - The string to copy to the clipboard.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Clipboard" )
static void CopyStringToClipboard(const FString& String);
/**
* Get the content from the clipboard as FText.
* @returns The text from the clipboard.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Clipboard" )
static FText GetTextFromClipboard();
/**
* Get the content from the clipboard as an FString.
* @returns The content from the clipboard as a string.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Clipboard" )
static FString GetStringFromClipboard();
/**
* Clear the clipboard.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Clipboard")
static void ClearClipboard();
/**
* Get the project version as a string.
* @returns The project version as a string.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Utility")
static FString GetProjectVersion();
/**
* Returns true if the game is running in the Unreal Editor.
* @note This will return false in packaged/standalone builds.
* @returns True if running in the editor.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Utility")
static bool IsRunningInEditor();
/**
* Checks whether a switch (e.g. "MySwitch" matching "-MySwitch") was passed on the
* process command line. Matching is case-insensitive.
* @param Switch - The switch name, without the leading dash.
* @returns True if the switch is present.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Utility")
static bool HasCommandLineSwitch(const FString& Switch);
/**
* Reads a key=value option (e.g. "MyKey" matching "-MyKey=Value") from the process
* command line. Matching is case-insensitive; quoted values are returned without the quotes.
* @param Key - The key name, without the leading dash or trailing equals sign.
* @param OutValue - [out] The option's value, or empty if the key is missing.
* @returns True if the key was present.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Utility")
static bool GetCommandLineOption(const FString& Key, FString& OutValue);
/** Core of Has Command Line Switch that checks an explicit command line. */
static bool HasCommandLineSwitch(const TCHAR* CommandLine, const FString& Switch);
/** Core of Get Command Line Option that reads from an explicit command line. */
static bool GetCommandLineOption(const TCHAR* CommandLine, const FString& Key, FString& OutValue);
};

View File

@@ -0,0 +1,124 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "GameplayTagContainer.h"
#include "DirectiveUtilGameplayTagFunctionLibrary.generated.h"
/**
* UDirectiveUtilGameplayTagFunctionLibrary
* Hierarchy navigation helpers for Gameplay Tags that the engine does not expose to Blueprints.
*/
UCLASS()
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilGameplayTagFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Returns the direct (immediate) parent of a tag, e.g. "A.B.C" returns "A.B".
* @param Tag - The tag to read.
* @returns The direct parent tag, or an invalid tag if the tag is a root or invalid.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags")
static FGameplayTag GetTagDirectParent(const FGameplayTag& Tag);
/**
* Returns all ancestor tags of a tag (its parents, grandparents, etc.), excluding the tag itself.
* e.g. "A.B.C" returns { "A.B", "A" }.
* @param Tag - The tag to read.
* @returns A container of the tag's ancestors.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags", meta = (BlueprintThreadSafe))
static FGameplayTagContainer GetTagParents(const FGameplayTag& Tag);
/**
* Returns the number of segments (hierarchy depth) of a tag, e.g. "A.B.C" returns 3.
* @param Tag - The tag to read.
* @returns The depth, or 0 for an invalid tag.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags", meta = (BlueprintThreadSafe))
static int32 GetTagDepth(const FGameplayTag& Tag);
/**
* Returns the last (leaf) segment of a tag's name, e.g. "A.B.C" returns "C".
* @param Tag - The tag to read.
* @returns The leaf segment, or an empty string for an invalid tag.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags", meta = (BlueprintThreadSafe))
static FString GetTagLeafName(const FGameplayTag& Tag);
/**
* Splits a tag's name into its individual segments, e.g. "A.B.C" returns [ "A", "B", "C" ].
* @param Tag - The tag to read.
* @returns The segments in order, or an empty array for an invalid tag.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags", meta = (BlueprintThreadSafe))
static TArray<FString> GetTagSegments(const FGameplayTag& Tag);
/**
* Returns all registered descendant tags of a tag (children, grandchildren, etc.), excluding the tag itself.
* e.g. "A" returns { "A.B", "A.B.C" } when those tags are registered.
* @param Tag - The tag to read.
* @returns A container of the tag's descendants, or an empty container for an invalid tag.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags", meta = (BlueprintThreadSafe))
static FGameplayTagContainer GetTagChildren(const FGameplayTag& Tag);
/**
* Returns only the direct (immediate) registered children of a tag, e.g. "A" returns "A.B" but not "A.B.C".
* @param Tag - The tag to read.
* @returns A container of the tag's direct children, or an empty container for an invalid tag.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags", meta = (BlueprintThreadSafe))
static FGameplayTagContainer GetTagDirectChildren(const FGameplayTag& Tag);
/**
* Returns the deepest tag that both tags share as an ancestor, e.g. "A.B.C" and "A.B.D" return "A.B".
* The result may be one of the inputs when one tag is an ancestor of the other.
* @param TagA - The first tag.
* @param TagB - The second tag.
* @returns The deepest common ancestor tag, or an invalid tag if the tags share none.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags")
static FGameplayTag GetTagCommonAncestor(const FGameplayTag& TagA, const FGameplayTag& TagB);
/**
* Truncates a tag to its first Depth segments, e.g. "A.B.C" at depth 2 returns "A.B".
* An ancestor of a registered tag is always registered itself.
* @param Tag - The tag to truncate.
* @param Depth - The number of leading segments to keep.
* @returns The truncated tag, the tag itself when Depth >= its depth, or an invalid tag when Depth < 1 or the tag is invalid.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags")
static FGameplayTag GetTagAtDepth(const FGameplayTag& Tag, int32 Depth);
/**
* Returns the registered tags that share a tag's direct parent, excluding the tag itself.
* Enumerating the siblings of a root tag (which has no parent) is not supported and returns an empty container.
* @param Tag - The tag to read.
* @returns A container of the tag's siblings, or an empty container for an invalid or root tag.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags")
static FGameplayTagContainer GetTagSiblings(const FGameplayTag& Tag);
/**
* Checks whether a tag has no registered children.
* @param Tag - The tag to test.
* @returns True if the tag is valid and has no registered descendants; false for an invalid tag.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags", meta = (BlueprintThreadSafe))
static bool IsLeafTag(const FGameplayTag& Tag);
/**
* Finds every registered gameplay tag whose name contains Substring (case-insensitive), in registry order.
* Cost scales with the size of the tag registry; intended for tooling and debug use, not per-frame calls.
* @param Substring - The text to search for. An empty string returns an empty array.
* @returns The matching tags.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags")
static TArray<FGameplayTag> FindRegisteredTags(const FString& Substring);
};

View File

@@ -0,0 +1,101 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Types/DirectiveUtilInputTypes.h"
#include "Types/DirectiveUtilTypes.h"
#include "DirectiveUtilInputFunctionLibrary.generated.h"
class UEnhancedInputLocalPlayerSubsystem;
/**
* UDirectiveUtilInputFunctionLibrary
* A function library that adds functionality for working with input in Unreal Engine.
*/
UCLASS()
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilInputFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Apply multiple Input Mapping Contexts.
* @param PlayerController The player controller to add the contexts to. Will attempt to get the LocalPlayer from the controller.
* @param Contexts The contexts to apply.
* @param bClearPrevious Whether to clear all previous contexts before applying the new ones.
* @returns Returns Success if the contexts were successfully applied.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Input", meta=(ExpandEnumAsExecs="ReturnValue", DefaultToSelf="PlayerController"))
static EDirectiveUtilSuccessStatus AddInputMappingContexts(
AController* PlayerController,
const TArray<FDirectiveUtilEnhancedInputContextData>& Contexts,
bool bClearPrevious);
/**
* Remove multiple Input Mapping Contexts.
* @param PlayerController The player controller to remove the contexts from. Will attempt to get the LocalPlayer from the controller.
* @param Contexts The contexts to remove.
* @returns Returns Success if the contexts were successfully removed.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Input", meta=(ExpandEnumAsExecs="ReturnValue", DefaultToSelf="PlayerController"))
static EDirectiveUtilSuccessStatus RemoveInputMappingContexts(
AController* PlayerController,
const TArray<TSoftObjectPtr<UInputMappingContext>>& Contexts);
/**
* Swap a designated Input Mapping Context with a new one.
* If the previous context is found, it will be removed and the new context will be added.
* If the previous context is not found, the new context will be added at the specified priority.
* @param PlayerController The player controller to swap the contexts on. Will attempt to get the LocalPlayer from the controller.
* @param PreviousContext The context to swap out.
* @param NewContext The context to swap in.
* @param Priority The priority to set the new context to.
* @param bUsePreviousPriority Whether to use the previous context's priority when adding the new context.
* @returns Returns Success if the contexts were successfully swapped.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Input", meta=(ExpandEnumAsExecs="ReturnValue", DefaultToSelf="PlayerController"))
static EDirectiveUtilSuccessStatus SwapInputMappingContexts(
AController* PlayerController,
TSoftObjectPtr<UInputMappingContext> PreviousContext,
TSoftObjectPtr<UInputMappingContext> NewContext,
int32 Priority,
bool bUsePreviousPriority);
/**
* Returns the Enhanced Input local player subsystem for the given controller, if available.
* @param PlayerController - The player controller; the Local Player's subsystem is retrieved.
* @returns The Enhanced Input subsystem, or null if the controller has no associated local player subsystem.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Input", meta=(DefaultToSelf="PlayerController"))
static UEnhancedInputLocalPlayerSubsystem* GetEnhancedInputSubsystem(AController* PlayerController);
/**
* Returns whether the given input mapping context is currently active on the controller.
* @param PlayerController - The player controller to query.
* @param Context - The input mapping context to check.
* @returns True if the context is currently applied.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Input", meta=(DefaultToSelf="PlayerController"))
static bool IsInputMappingContextActive(AController* PlayerController, TSoftObjectPtr<UInputMappingContext> Context);
/**
* Removes all input mapping contexts from the controller.
* @param PlayerController - The player controller to clear.
* @returns Success if the contexts were cleared.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Input", meta=(ExpandEnumAsExecs="ReturnValue", DefaultToSelf="PlayerController"))
static EDirectiveUtilSuccessStatus ClearAllInputMappingContexts(AController* PlayerController);
protected:
/**
* Attempt to get the Enhanced Input Subsystem from the provided controller.
* @param PlayerController - The player controller to get the subsystem from.
* @param EnhancedInput - The Enhanced Input Subsystem to return
* @return True if the subsystem was found.
*/
static bool TryGetEnhancedInputSubsystemFromController(
AController* PlayerController,
UEnhancedInputLocalPlayerSubsystem*& EnhancedInput);
};

View File

@@ -0,0 +1,291 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Net/Core/PushModel/PushModel.h"
#include "DirectiveUtilMapFunctionLibrary.generated.h"
/**
* UDirectiveUtilMapFunctionLibrary
* A collection of map (TMap) utility functions that improve the usability of maps in Blueprints.
*/
UCLASS()
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilMapFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Finds the value associated with Key, adding a new default-constructed entry if the key is not present.
* @param TargetMap - The map to search or add to.
* @param Key - The key to look up.
* @param Value - [out] A copy of the existing or newly added value.
*/
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Find Or Add", CompactNodeTitle = "FIND OR ADD", MapParam = "TargetMap", MapKeyParam = "Key", MapValueParam = "Value", AutoCreateRefTerm = "Key, Value"), Category="Directive Utilities|Map")
static void Map_FindOrAdd(const TMap<int32, int32>& TargetMap, const int32& Key, int32& Value);
/**
* Resets every value in the map to its default while preserving all keys.
* @param TargetMap - The map whose values will be reset.
*/
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Clear Values", CompactNodeTitle = "CLEAR VALUES", MapParam = "TargetMap"), Category="Directive Utilities|Map")
static void Map_ClearValues(const TMap<int32, int32>& TargetMap);
/**
* Gathers every key whose value is identical to Value, in map order.
* @param TargetMap - The map to search.
* @param Value - The value to look for.
* @param Keys - [out] Every key associated with Value.
*/
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Get Keys By Value", CompactNodeTitle = "KEYS BY VALUE", MapParam = "TargetMap", MapValueParam = "Value", MapKeyParam = "Keys", AutoCreateRefTerm = "Value, Keys"), Category="Directive Utilities|Map")
static void Map_GetKeysByValue(const TMap<int32, int32>& TargetMap, const int32& Value, TArray<int32>& Keys);
/**
* Checks whether any value in the map is identical to Value.
* @param TargetMap - The map to search.
* @param Value - The value to look for.
* @returns True if at least one entry holds Value.
*/
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Has Value", CompactNodeTitle = "HAS VALUE", MapParam = "TargetMap", MapValueParam = "Value", AutoCreateRefTerm = "Value"), Category="Directive Utilities|Map")
static bool Map_HasValue(const TMap<int32, int32>& TargetMap, const int32& Value);
/**
* Removes every key in Keys from the map.
* @param TargetMap - The map to remove from.
* @param Keys - The keys to remove.
* @returns The number of entries that were actually removed.
*/
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Remove Keys", CompactNodeTitle = "REMOVE KEYS", MapParam = "TargetMap", MapKeyParam = "Keys", AutoCreateRefTerm = "Keys"), Category="Directive Utilities|Map")
static int32 Map_RemoveKeys(const TMap<int32, int32>& TargetMap, const TArray<int32>& Keys);
/**
* Copies every pair from SourceMap into TargetMap. Both maps must share key and value types.
* @param TargetMap - The map to copy into.
* @param SourceMap - The map to copy from.
* @param bOverwriteExisting - If true, keys already present in TargetMap are overwritten with SourceMap's values.
*/
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Append", CompactNodeTitle = "APPEND", MapParam = "TargetMap|SourceMap"), Category="Directive Utilities|Map")
static void Map_Append(const TMap<int32, int32>& TargetMap, const TMap<int32, int32>& SourceMap, bool bOverwriteExisting = true);
/*~
* Native functions called by the custom thunk layers below, which read off the property address
* and operate on the underlying map. Based off UBlueprintMapLibrary implementation.
~*/
static void GenericMap_FindOrAdd(const void* TargetMap, const FMapProperty* MapProperty, const void* KeyPtr, void* ValuePtr);
static void GenericMap_ClearValues(const void* TargetMap, const FMapProperty* MapProperty);
static void GenericMap_GetKeysByValue(const void* TargetMap, const FMapProperty* MapProperty, const void* ValuePtr, const void* TargetArray, const FArrayProperty* ArrayProperty);
static bool GenericMap_HasValue(const void* TargetMap, const FMapProperty* MapProperty, const void* ValuePtr);
static int32 GenericMap_RemoveKeys(const void* TargetMap, const FMapProperty* MapProperty, const void* TargetArray, const FArrayProperty* ArrayProperty);
static void GenericMap_Append(const void* TargetMap, const FMapProperty* TargetMapProperty, const void* SourceMap, const FMapProperty* SourceMapProperty, bool bOverwriteExisting);
/*~
* Custom thunk layers that read off the property address and call the appropriate native handler.
* Based off UBlueprintMapLibrary implementation.
~*/
DECLARE_FUNCTION(execMap_FindOrAdd)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FMapProperty>(nullptr);
void* MapAddr = Stack.MostRecentPropertyAddress;
FMapProperty* MapProperty = CastField<FMapProperty>(Stack.MostRecentProperty);
if (!MapProperty)
{
Stack.bArrayContextFailed = true;
return;
}
const FProperty* CurrKeyProp = MapProperty->KeyProp;
const int32 KeyPropertySize = CurrKeyProp->GetElementSize() * CurrKeyProp->ArrayDim;
void* KeyStorageSpace = FMemory_Alloca(KeyPropertySize);
CurrKeyProp->InitializeValue(KeyStorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(KeyStorageSpace);
const FProperty* CurrValueProp = MapProperty->ValueProp;
const int32 ValuePropertySize = CurrValueProp->GetElementSize() * CurrValueProp->ArrayDim;
void* ValueStorageSpace = FMemory_Alloca(ValuePropertySize);
CurrValueProp->InitializeValue(ValueStorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(ValueStorageSpace);
void* ItemPtr;
if (Stack.MostRecentPropertyAddress != nullptr && Stack.MostRecentProperty != nullptr
&& ValuePropertySize == Stack.MostRecentProperty->GetElementSize() * Stack.MostRecentProperty->ArrayDim
&& (Stack.MostRecentProperty->GetClass()->IsChildOf(CurrValueProp->GetClass())
|| CurrValueProp->GetClass()->IsChildOf(Stack.MostRecentProperty->GetClass())))
{
ItemPtr = Stack.MostRecentPropertyAddress;
}
else
{
ItemPtr = ValueStorageSpace;
}
P_FINISH;
P_NATIVE_BEGIN;
MARK_PROPERTY_DIRTY(Stack.Object, MapProperty);
GenericMap_FindOrAdd(MapAddr, MapProperty, KeyStorageSpace, ItemPtr);
P_NATIVE_END;
CurrValueProp->DestroyValue(ValueStorageSpace);
CurrKeyProp->DestroyValue(KeyStorageSpace);
}
DECLARE_FUNCTION(execMap_ClearValues)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FMapProperty>(nullptr);
void* MapAddr = Stack.MostRecentPropertyAddress;
FMapProperty* MapProperty = CastField<FMapProperty>(Stack.MostRecentProperty);
if (!MapProperty)
{
Stack.bArrayContextFailed = true;
return;
}
P_FINISH;
P_NATIVE_BEGIN;
MARK_PROPERTY_DIRTY(Stack.Object, MapProperty);
GenericMap_ClearValues(MapAddr, MapProperty);
P_NATIVE_END;
}
DECLARE_FUNCTION(execMap_GetKeysByValue)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FMapProperty>(nullptr);
void* MapAddr = Stack.MostRecentPropertyAddress;
FMapProperty* MapProperty = CastField<FMapProperty>(Stack.MostRecentProperty);
if (!MapProperty)
{
Stack.bArrayContextFailed = true;
return;
}
const FProperty* CurrValueProp = MapProperty->ValueProp;
const int32 ValuePropertySize = CurrValueProp->GetElementSize() * CurrValueProp->ArrayDim;
void* ValueStorageSpace = FMemory_Alloca(ValuePropertySize);
CurrValueProp->InitializeValue(ValueStorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(ValueStorageSpace);
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
void* ArrayAddr = Stack.MostRecentPropertyAddress;
FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty || !ArrayProperty->Inner->SameType(MapProperty->KeyProp))
{
Stack.bArrayContextFailed = true;
CurrValueProp->DestroyValue(ValueStorageSpace);
return;
}
P_FINISH;
P_NATIVE_BEGIN;
GenericMap_GetKeysByValue(MapAddr, MapProperty, ValueStorageSpace, ArrayAddr, ArrayProperty);
P_NATIVE_END;
CurrValueProp->DestroyValue(ValueStorageSpace);
}
DECLARE_FUNCTION(execMap_HasValue)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FMapProperty>(nullptr);
void* MapAddr = Stack.MostRecentPropertyAddress;
FMapProperty* MapProperty = CastField<FMapProperty>(Stack.MostRecentProperty);
if (!MapProperty)
{
Stack.bArrayContextFailed = true;
return;
}
const FProperty* CurrValueProp = MapProperty->ValueProp;
const int32 ValuePropertySize = CurrValueProp->GetElementSize() * CurrValueProp->ArrayDim;
void* ValueStorageSpace = FMemory_Alloca(ValuePropertySize);
CurrValueProp->InitializeValue(ValueStorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(ValueStorageSpace);
P_FINISH;
P_NATIVE_BEGIN;
*static_cast<bool*>(RESULT_PARAM) = GenericMap_HasValue(MapAddr, MapProperty, ValueStorageSpace);
P_NATIVE_END;
CurrValueProp->DestroyValue(ValueStorageSpace);
}
DECLARE_FUNCTION(execMap_RemoveKeys)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FMapProperty>(nullptr);
void* MapAddr = Stack.MostRecentPropertyAddress;
FMapProperty* MapProperty = CastField<FMapProperty>(Stack.MostRecentProperty);
if (!MapProperty)
{
Stack.bArrayContextFailed = true;
return;
}
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
void* ArrayAddr = Stack.MostRecentPropertyAddress;
FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty || !ArrayProperty->Inner->SameType(MapProperty->KeyProp))
{
Stack.bArrayContextFailed = true;
return;
}
P_FINISH;
P_NATIVE_BEGIN;
MARK_PROPERTY_DIRTY(Stack.Object, MapProperty);
*static_cast<int32*>(RESULT_PARAM) = GenericMap_RemoveKeys(MapAddr, MapProperty, ArrayAddr, ArrayProperty);
P_NATIVE_END;
}
DECLARE_FUNCTION(execMap_Append)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FMapProperty>(nullptr);
void* TargetMapAddr = Stack.MostRecentPropertyAddress;
FMapProperty* TargetMapProperty = CastField<FMapProperty>(Stack.MostRecentProperty);
if (!TargetMapProperty)
{
Stack.bArrayContextFailed = true;
return;
}
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FMapProperty>(nullptr);
void* SourceMapAddr = Stack.MostRecentPropertyAddress;
FMapProperty* SourceMapProperty = CastField<FMapProperty>(Stack.MostRecentProperty);
if (!SourceMapProperty
|| !SourceMapProperty->KeyProp->SameType(TargetMapProperty->KeyProp)
|| !SourceMapProperty->ValueProp->SameType(TargetMapProperty->ValueProp))
{
Stack.bArrayContextFailed = true;
return;
}
P_GET_UBOOL(bOverwriteExisting);
P_FINISH;
P_NATIVE_BEGIN;
MARK_PROPERTY_DIRTY(Stack.Object, TargetMapProperty);
GenericMap_Append(TargetMapAddr, TargetMapProperty, SourceMapAddr, SourceMapProperty, bOverwriteExisting);
P_NATIVE_END;
}
};

View File

@@ -0,0 +1,244 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Types/DirectiveUtilMathTypes.h"
#include "DirectiveUtilMathFunctionLibrary.generated.h"
/**
* UDirectiveUtilMathFunctionLibrary
*
* Contains math functions for the Directive Utilities plugin.
*/
UCLASS()
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilMathFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Returns a perlin noise value between -1 and 1 at the given position.
* @note This exposes the built-in PerlinNoise2D function to blueprints.
* @param Position - The position to get the noise value for.
* @returns The noise value at the given position.
*/
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Random")
static float PerlinNoise2D(FVector2D Position);
/**
* Returns a perlin noise value between -1 and 1 at the given position.
* @note This exposes the built-in PerlinNoise3D function to blueprints.
* @param Position - The position to get the noise value for.
* @returns The noise value at the given position.
*/
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Random")
static float PerlinNoise3D(const FVector& Position);
/**
* Returns the angle in degrees between two vectors.
* @param A - The first vector.
* @param B - The second vector.
* @returns The angle between the two vectors in degrees.
*/
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Vector")
static float AngleBetweenVectors(const FVector& A, const FVector& B);
/**
* Applies a Back/Elastic/Bounce easing curve to a normalized alpha.
* @note These are the Penner easing curves the engine's built-in "Ease" node (EEasingFunc) does not provide.
* For Sinusoidal/Exponential/Circular/power easings, use the engine's "Ease" node instead.
* @param Alpha - The input alpha. Clamped to the [0, 1] range.
* @param EaseType - The easing curve to apply.
* @returns The eased alpha. Note that Back and Elastic curves intentionally overshoot the [0, 1] range.
*/
UFUNCTION(BlueprintPure, meta = (DisplayName = "Ease Alpha", BlueprintThreadSafe), Category = "Directive Utilities|Math|Easing")
static float EaseAlpha(float Alpha, EDirectiveUtilEaseType EaseType);
/**
* Eases a float from A to B using a Back/Elastic/Bounce easing curve.
* @param A - The start value (returned at Alpha 0).
* @param B - The target value (returned at Alpha 1).
* @param Alpha - The input alpha. Clamped to the [0, 1] range.
* @param EaseType - The easing curve to apply.
* @returns The eased value between A and B.
*/
UFUNCTION(BlueprintPure, meta = (DisplayName = "Ease (Float)", BlueprintThreadSafe), Category = "Directive Utilities|Math|Easing")
static float EaseFloat(float A, float B, float Alpha, EDirectiveUtilEaseType EaseType);
/**
* Eases a vector from A to B using a Back/Elastic/Bounce easing curve.
* @param A - The start vector (returned at Alpha 0).
* @param B - The target vector (returned at Alpha 1).
* @param Alpha - The input alpha. Clamped to the [0, 1] range.
* @param EaseType - The easing curve to apply.
* @returns The eased vector between A and B.
*/
UFUNCTION(BlueprintPure, meta = (DisplayName = "Ease (Vector)", BlueprintThreadSafe), Category = "Directive Utilities|Math|Easing")
static FVector EaseVector(const FVector& A, const FVector& B, float Alpha, EDirectiveUtilEaseType EaseType);
/**
* Eases a rotator from A to B using a Back/Elastic/Bounce easing curve (shortest-path interpolation).
* @param A - The start rotator (returned at Alpha 0).
* @param B - The target rotator (returned at Alpha 1).
* @param Alpha - The input alpha. Clamped to the [0, 1] range.
* @param EaseType - The easing curve to apply.
* @returns The eased rotator between A and B.
*/
UFUNCTION(BlueprintPure, meta = (DisplayName = "Ease (Rotator)", BlueprintThreadSafe), Category = "Directive Utilities|Math|Easing")
static FRotator EaseRotator(const FRotator& A, const FRotator& B, float Alpha, EDirectiveUtilEaseType EaseType);
/**
* Eases a color from A to B using a Back/Elastic/Bounce easing curve.
* @param A - The start color (returned at Alpha 0).
* @param B - The target color (returned at Alpha 1).
* @param Alpha - The input alpha. Clamped to the [0, 1] range.
* @param EaseType - The easing curve to apply.
* @returns The eased color between A and B.
*/
UFUNCTION(BlueprintPure, meta = (DisplayName = "Ease (Color)", BlueprintThreadSafe), Category = "Directive Utilities|Math|Easing")
static FLinearColor EaseColor(const FLinearColor& A, const FLinearColor& B, float Alpha, EDirectiveUtilEaseType EaseType);
/**
* Rounds a float to a given number of decimal places. Rounds half away from zero,
* matching "Round To Decimals (Text)".
* @note Due to floating-point representation the returned value may not display exactly;
* use "Round To Decimals (Text)" for clean display.
* @param Value - The value to round.
* @param Decimals - The number of decimal places to round to. Clamped to the [0, 10] range.
* @returns The rounded value.
*/
UFUNCTION(BlueprintPure, meta = (DisplayName = "Round To Decimals", BlueprintThreadSafe), Category = "Directive Utilities|Math|Float")
static float RoundToDecimals(float Value, int32 Decimals);
/**
* Rounds a float to a given number of decimal places and returns it as display text.
* Rounds half away from zero, matching "Round To Decimals".
* @param Value - The value to round.
* @param Decimals - The maximum number of decimal places to display. Clamped to the [0, 10] range.
* @returns The rounded value as text, formatted with the current locale.
*/
UFUNCTION(BlueprintPure, meta = (DisplayName = "Round To Decimals (Text)", BlueprintThreadSafe), Category = "Directive Utilities|Math|Float")
static FText RoundToDecimalsAsText(float Value, int32 Decimals);
/**
* Formats a byte count as a human-readable size using binary units (1024): B, KB, MB, GB, TB, PB.
* Decimals are applied only from KB up ("532 B", "1.4 MB"). Negative input formats the absolute
* value with a leading minus sign. Output is English-only.
* @param Bytes - The byte count to format.
* @param Decimals - The number of decimal places to show from KB up. Clamped to the [0, 3] range.
* @returns The formatted size text.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Math|Formatting")
static FText FormatBytes(int64 Bytes, int32 Decimals = 1);
/**
* Formats a duration in seconds as d/h/m/s units from the largest nonzero unit down, with
* two-digit padding after the first ("1h 03m 05s", "2d 04h", "45s"). With bIncludeSeconds
* false the seconds unit is dropped and sub-minute durations return "0m". Negative input gets
* a leading minus sign; non-finite input returns "0s". Output is English-only.
* @param Seconds - The duration in seconds.
* @param bIncludeSeconds - Whether to include the seconds unit.
* @returns The formatted duration text.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Math|Formatting")
static FText FormatDuration(float Seconds, bool bIncludeSeconds = true);
/**
* Formats a timestamp relative to the current local time: "just now" (under a minute),
* "N minute(s)/hour(s)/day(s) ago", or "in N ..." for future timestamps. Uses local time,
* pairing with Get Save Slot Timestamp. Output is English-only.
* @note Not pure: reads the current clock each call.
* @param Timestamp - The local timestamp to describe.
* @returns The formatted relative time text.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Math|Formatting")
static FText FormatRelativeTime(const FDateTime& Timestamp);
/**
* Returns the sum of an integer array as a 64-bit integer, so large arrays cannot overflow int32.
* @param Values - The values to sum.
* @returns The sum of the values, or 0 if the array is empty.
*/
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
static int64 GetIntArraySum(const TArray<int32>& Values);
/**
* Returns the arithmetic mean of an integer array.
* @param Values - The values to average.
* @returns The average of the values, or 0 if the array is empty.
*/
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
static float GetIntArrayAverage(const TArray<int32>& Values);
/**
* Returns the median of an integer array (computed on a sorted copy; the input is not modified).
* For an even count, returns the average of the two middle values.
* @param Values - The values to take the median of.
* @returns The median of the values, or 0 if the array is empty.
*/
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
static float GetIntArrayMedian(const TArray<int32>& Values);
/**
* Returns the population standard deviation of an integer array (divides by N, not N-1).
* @param Values - The values to measure.
* @returns The population standard deviation, or 0 if the array is empty.
*/
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
static float GetIntArrayStandardDeviation(const TArray<int32>& Values);
/**
* Returns the sum of a float array. Accumulates in double internally for precision.
* @param Values - The values to sum.
* @returns The sum of the values, or 0 if the array is empty.
*/
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
static float GetFloatArraySum(const TArray<float>& Values);
/**
* Returns the arithmetic mean of a float array. Accumulates in double internally for precision.
* @param Values - The values to average.
* @returns The average of the values, or 0 if the array is empty.
*/
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
static float GetFloatArrayAverage(const TArray<float>& Values);
/**
* Returns the median of a float array (computed on a sorted copy; the input is not modified).
* For an even count, returns the average of the two middle values.
* @param Values - The values to take the median of.
* @returns The median of the values, or 0 if the array is empty.
*/
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
static float GetFloatArrayMedian(const TArray<float>& Values);
/**
* Returns the population standard deviation of a float array (divides by N, not N-1).
* Accumulates in double internally for precision.
* @param Values - The values to measure.
* @returns The population standard deviation, or 0 if the array is empty.
*/
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
static float GetFloatArrayStandardDeviation(const TArray<float>& Values);
/**
* Returns a random index into the Weights array, where each index's probability is proportional to its weight.
* Useful for loot tables and weighted spawning. Negative weights are treated as zero.
* @param Weights - The per-index weights.
* @returns The selected index, or INDEX_NONE (-1) if the array is empty or all weights are zero.
*/
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Get Random Index From Weights"), Category = "Directive Utilities|Math|Random")
static int32 GetRandomIndexFromWeights(const TArray<float>& Weights);
/**
* Deterministic version of Get Random Index From Weights that draws from (and advances) the provided random stream.
* @param Stream - The random stream to draw from.
* @param Weights - The per-index weights. Negative weights are treated as zero.
* @returns The selected index, or INDEX_NONE (-1) if the array is empty or all weights are zero.
*/
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Get Random Index From Weights (Stream)"), Category = "Directive Utilities|Math|Random")
static int32 GetRandomIndexFromWeightsFromStream(UPARAM(ref) FRandomStream& Stream, const TArray<float>& Weights);
};

View File

@@ -0,0 +1,80 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "DirectiveUtilRegexFunctionLibrary.generated.h"
/**
* UDirectiveUtilRegexFunctionLibrary
* Exposes the engine's regular-expression matching (FRegexPattern/FRegexMatcher) to Blueprints and Python.
* @warning Complex patterns with nested quantifiers can be extremely slow on long inputs
* (catastrophic backtracking), and matching runs synchronously on the calling thread.
*/
UCLASS()
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilRegexFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Returns true if the pattern matches anywhere within the input string.
* @param Input - The string to search.
* @param Pattern - The regular expression pattern.
* @param bCaseSensitive - Whether matching is case-sensitive. Defaults to true.
* @returns True if at least one match was found.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Regex")
static bool RegexMatches(const FString& Input, const FString& Pattern, bool bCaseSensitive = true);
/**
* Finds the first match of the pattern within the input string.
* @param Input - The string to search.
* @param Pattern - The regular expression pattern.
* @param OutMatch - [out] The matched substring, or empty if no match.
* @param OutMatchStart - [out] The start index of the match, or INDEX_NONE if no match.
* @param OutMatchEnd - [out] The index just past the end of the match, or INDEX_NONE if no match.
* @param bCaseSensitive - Whether matching is case-sensitive. Defaults to true.
* @returns True if a match was found.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Regex")
static bool RegexFindFirst(const FString& Input, const FString& Pattern, FString& OutMatch, int32& OutMatchStart, int32& OutMatchEnd, bool bCaseSensitive = true);
/**
* Returns every (non-overlapping) match of the pattern within the input string.
* @note Zero-width (empty) matches are ignored.
* @param Input - The string to search.
* @param Pattern - The regular expression pattern.
* @param bCaseSensitive - Whether matching is case-sensitive. Defaults to true.
* @returns The matched substrings, in order.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Regex")
static TArray<FString> RegexFindAll(const FString& Input, const FString& Pattern, bool bCaseSensitive = true);
/**
* Replaces every match of the pattern in the input string with a literal replacement.
* @note The replacement is literal text; capture-group back-references (e.g. $1) are not expanded.
* @note Zero-width (empty) matches are ignored.
* @param Input - The string to operate on.
* @param Pattern - The regular expression pattern.
* @param Replacement - The literal text to substitute for each match.
* @param bCaseSensitive - Whether matching is case-sensitive. Defaults to true.
* @returns The string with all matches replaced.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Regex")
static FString RegexReplaceAll(const FString& Input, const FString& Pattern, const FString& Replacement, bool bCaseSensitive = true);
/**
* Returns a specific capture group from the first match of the pattern.
* @param Input - The string to search.
* @param Pattern - The regular expression pattern.
* @param GroupIndex - The capture group to retrieve. 0 is the entire match; 1+ are the parenthesized groups.
* @param OutGroup - [out] The captured substring, or empty if the group did not participate in the match.
* @param bCaseSensitive - Whether matching is case-sensitive. Defaults to true.
* @returns True if a match was found and the requested group participated in it.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Regex")
static bool RegexGetCaptureGroup(const FString& Input, const FString& Pattern, int32 GroupIndex, FString& OutGroup, bool bCaseSensitive = true);
};

View File

@@ -0,0 +1,93 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "DirectiveUtilSaveGameFunctionLibrary.generated.h"
class USaveGame;
/**
* UDirectiveUtilSaveGameFunctionLibrary
* Save-slot utilities that fill the gaps left by UGameplayStatics: enumerating slots, reading slot
* timestamps, and serializing a save object to/from an in-memory byte array. This is slot/IO QoL only,
* not a save framework: use the engine's SaveGameToSlot/LoadGameFromSlot for the actual slot I/O.
*/
UCLASS()
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilSaveGameFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Returns the names of all existing save slots in the project's default save directory.
* @note This enumerates the engine's default file-based save directory (Saved/SaveGames); it does not
* cover platform-specific save systems (e.g. console storage).
* @returns The save slot names (without extension).
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|SaveGame")
static TArray<FString> GetAllSaveSlotNames();
/**
* Returns the last-modified timestamp of a save slot, if it exists.
* @param SlotName - The save slot name.
* @param OutTimestamp - [out] The slot's last-modified time (local), or a default time if it does not exist.
* Converted from the file system's UTC timestamp to local time.
* @returns True if the slot exists.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|SaveGame")
static bool GetSaveSlotTimestamp(const FString& SlotName, FDateTime& OutTimestamp);
/**
* Serializes a save game object to an in-memory byte array (instead of a slot file).
* Useful for custom storage, networking, or cloud saves.
* @param SaveGameObject - The save game object to serialize.
* @param OutBytes - [out] The serialized bytes.
* @returns True if serialization succeeded.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|SaveGame")
static bool SaveGameToBytes(USaveGame* SaveGameObject, TArray<uint8>& OutBytes);
/**
* Deserializes a save game object from an in-memory byte array produced by Save Game To Bytes.
* @param SaveData - The serialized bytes.
* @returns The deserialized save game object, or null on failure.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|SaveGame")
static USaveGame* LoadGameFromBytes(const TArray<uint8>& SaveData);
/**
* Checks whether a save slot exists. Goes through the engine's save game system, so unlike
* enumeration it also works on platform save backends.
* @param SlotName - The save slot name.
* @param UserIndex - The platform user index the save belongs to.
* @returns True if the slot exists.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|SaveGame")
static bool DoesSaveSlotExist(const FString& SlotName, int32 UserIndex = 0);
/**
* Deletes a save slot. Goes through the engine's save game system, so unlike enumeration it
* also works on platform save backends.
* @param SlotName - The save slot name.
* @param UserIndex - The platform user index the save belongs to.
* @returns True if a save was actually deleted.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|SaveGame")
static bool DeleteSaveSlot(const FString& SlotName, int32 UserIndex = 0);
/**
* Renames a save slot by copying its data to the new name and then deleting the original.
* Fails without mutating anything unless both names are valid, the names differ, the old slot
* exists, and the new slot does not. On failure the original slot is never lost. Goes through
* the engine's save game system, so unlike enumeration it also works on platform save backends.
* @param OldSlotName - The existing save slot name.
* @param NewSlotName - The new save slot name.
* @param UserIndex - The platform user index the save belongs to.
* @returns True if the slot was renamed.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|SaveGame")
static bool RenameSaveSlot(const FString& OldSlotName, const FString& NewSlotName, int32 UserIndex = 0);
};

View File

@@ -0,0 +1,339 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Types/DirectiveUtilTypes.h"
#include "DirectiveUtilStringFunctionLibrary.generated.h"
/**
* UDirectiveUtilStringFunctionLibrary
* A collection of helpful string utility functions that improve the usability of strings in Blueprints.
*/
UCLASS()
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilStringFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Detect if the provided string contains any letters.
* @note Operates on ASCII characters only; non-ASCII letters/digits are not classified or cased.
* @param String - The string to check.
* @returns True if the string contains letters.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String" )
static bool ContainsLetters(const FString& String);
/**
* Detect if the provided string contains any numbers.
* @note Operates on ASCII characters only; non-ASCII letters/digits are not classified or cased.
* @param String - The string to check.
* @returns True if the string contains numbers.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String" )
static bool ContainsNumbers(const FString& String);
/**
* Detect if the provided string contains any spaces.
* @param String - The string to check.
* @returns True if the string contains spaces.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String" )
static bool ContainsSpaces(const FString& String);
/**
* Detect if the provided string contains any special characters.
* @note Operates on ASCII characters only; non-ASCII letters/digits are not classified or cased.
* @param String - The string to check.
* @returns True if the string contains special characters.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String" )
static bool ContainsSpecialCharacters(const FString& String);
/**
* Filter out characters types from the string.
* @note Operates on ASCII characters only; non-ASCII letters/digits are not classified or cased.
* @param String - The string to filter.
* @param bLetters - If true, filter out letters.
* @param bNumbers - If true, filter out numbers.
* @param bSpecialCharacters - If true, filter out special characters.
* @param bSpaces - If true, filter out spaces.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String" )
static FString FilterCharacters(const FString& String, const bool bLetters, const bool bNumbers, const bool bSpecialCharacters, const bool bSpaces);
/**
* Sort a string array alphabetically.
* @param StringArray - The string array to sort.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String", meta = (DeprecatedFunction, DeprecationMessage = "Use the engine's Sort String Array node (UE 5.6+) instead."))
static TArray<FString> SortStringArray(TArray<FString> StringArray);
/**
* Truncates a string to the specified length and appends a suffix.
* @param String - The string to truncate.
* @param MaxLength - The maximum length of the resulting string including the suffix. If MaxLength is smaller than the suffix length, the full suffix is still returned.
* @param Suffix - The suffix to append when truncated.
* @returns The truncated string.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString TruncateString(const FString& String, int32 MaxLength, const FString& Suffix = TEXT("..."));
/**
* Converts a string to title case, capitalizing the first letter of each word.
* @note Operates on ASCII characters only; non-ASCII letters/digits are not classified or cased.
* @param String - The string to convert.
* @returns The title-cased string.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString ToTitleCase(const FString& String);
/**
* Splits a string into its component words. Words are delimited by any non-alphanumeric
* character (which is consumed), a lower-to-upper transition ("fooBar" -> "foo", "Bar"),
* the end of an acronym ("XMLParser" -> "XML", "Parser"), or a letter/digit transition
* ("version2Beta" -> "version", "2", "Beta").
* @note Operates on ASCII characters only; non-ASCII letters/digits are not classified or cased.
* @param String - The string to split.
* @returns The words in order, or an empty array for an empty input.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static TArray<FString> SplitIntoWords(const FString& String);
/**
* Converts a string to camelCase: the first word lowercased, every following word capitalized,
* joined without separators ("my var name" -> "myVarName").
* @note Operates on ASCII characters only; non-ASCII letters/digits are not classified or cased.
* @param String - The string to convert.
* @returns The camelCased string.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString ToCamelCase(const FString& String);
/**
* Converts a string to PascalCase: every word capitalized, joined without separators
* ("my var name" -> "MyVarName").
* @note Operates on ASCII characters only; non-ASCII letters/digits are not classified or cased.
* @param String - The string to convert.
* @returns The PascalCased string.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString ToPascalCase(const FString& String);
/**
* Converts a string to snake_case: every word lowercased, joined with underscores
* ("my var name" -> "my_var_name").
* @note Operates on ASCII characters only; non-ASCII letters/digits are not classified or cased.
* @param String - The string to convert.
* @returns The snake_cased string.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString ToSnakeCase(const FString& String);
/**
* Converts a string to kebab-case: every word lowercased, joined with hyphens
* ("my var name" -> "my-var-name").
* @note Operates on ASCII characters only; non-ASCII letters/digits are not classified or cased.
* @param String - The string to convert.
* @returns The kebab-cased string.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString ToKebabCase(const FString& String);
/**
* Returns a sorted copy of the provided string array.
* @param StringArray - The array of strings to sort.
* @returns A sorted copy of the provided string array.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|String", meta = (DeprecatedFunction, DeprecationMessage = "Use the engine's Sort String Array node (UE 5.6+) instead."))
static TArray<FString> GetSortedStringArray(TArray<FString> StringArray);
/**
* Returns the Levenshtein (edit) distance between two strings: the minimum number of single-character
* insertions, deletions, or substitutions needed to turn one string into the other.
* @param A - The first string.
* @param B - The second string.
* @param bCaseSensitive - Whether the comparison is case-sensitive. Defaults to true.
* @returns The edit distance (0 means identical).
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static int32 GetLevenshteinDistance(const FString& A, const FString& B, bool bCaseSensitive = true);
/**
* Returns a normalized similarity score between two strings, from 0 (completely different) to 1 (identical),
* derived from the Levenshtein distance. Two empty strings are considered identical.
* @param A - The first string.
* @param B - The second string.
* @param bCaseSensitive - Whether the comparison is case-sensitive. Defaults to true.
* @returns The similarity in the [0, 1] range.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static float GetStringSimilarity(const FString& A, const FString& B, bool bCaseSensitive = true);
/**
* Returns true if the source string contains any of the provided search terms. Empty terms are ignored.
* @param Source - The string to search.
* @param SearchTerms - The substrings to look for.
* @param bCaseSensitive - Whether the search is case-sensitive. Defaults to true.
* @returns True if at least one non-empty term is found.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static bool ContainsAny(const FString& Source, const TArray<FString>& SearchTerms, bool bCaseSensitive = true);
/**
* Finds the earliest occurrence in the source string of any of the provided search terms.
* @param Source - The string to search.
* @param SearchTerms - The substrings to look for. Empty terms are ignored.
* @param bCaseSensitive - Whether the search is case-sensitive. Defaults to true.
* @param OutFoundIndex - [out] The index in Source of the earliest match, or INDEX_NONE if none.
* @param OutTermIndex - [out] The index into SearchTerms of the matched term, or INDEX_NONE if none.
* @returns True if any term was found.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static bool FindFirstOfAny(const FString& Source, const TArray<FString>& SearchTerms, bool bCaseSensitive, int32& OutFoundIndex, int32& OutTermIndex);
/**
* Encodes a string to Base64.
* @param Source - The string to encode.
* @returns The Base64-encoded string.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString Base64Encode(const FString& Source);
/**
* Decodes a Base64 string.
* @param Source - The Base64 string to decode.
* @param OutDecoded - [out] The decoded string, or empty on failure.
* @returns True if the input was valid Base64 and was decoded.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static bool Base64Decode(const FString& Source, FString& OutDecoded);
/**
* Encodes a string as lowercase hex. The string is converted to UTF-8 bytes first.
* @param String - The string to encode.
* @returns The lowercase hex encoding of the string's UTF-8 bytes.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString HexEncode(const FString& String);
/**
* Decodes a hex string (either case accepted) into the string its bytes spell in UTF-8.
* @param Hex - The hex string to decode.
* @param OutString - [out] The decoded string, or empty on failure.
* @returns True if the input was valid even-length hex and was decoded.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static bool HexDecode(const FString& Hex, FString& OutString);
/**
* Encodes a byte array as lowercase hex.
* @param Bytes - The bytes to encode.
* @returns The lowercase hex encoding of the bytes.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString HexEncodeBytes(const TArray<uint8>& Bytes);
/**
* Decodes a hex string (either case accepted) into a byte array.
* @param Hex - The hex string to decode.
* @param OutBytes - [out] The decoded bytes, or empty on failure.
* @returns True if the input was valid even-length hex and was decoded.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static bool HexDecodeBytes(const FString& Hex, TArray<uint8>& OutBytes);
/**
* Returns the MD5 digest of the string as 32 lowercase hex characters. The string is
* converted to UTF-8 bytes first.
* @note For integrity checks and cache keys, not for security or password storage.
* @param String - The string to hash.
* @returns The MD5 digest as lowercase hex.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString Md5HashString(const FString& String);
/**
* Returns the MD5 digest of a byte array as 32 lowercase hex characters.
* @note For integrity checks and cache keys, not for security or password storage.
* @param Bytes - The bytes to hash.
* @returns The MD5 digest as lowercase hex.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString Md5HashBytes(const TArray<uint8>& Bytes);
/**
* Returns the SHA-1 digest of the string as 40 lowercase hex characters. The string is
* converted to UTF-8 bytes first.
* @note For integrity checks and cache keys, not for security or password storage.
* @param String - The string to hash.
* @returns The SHA-1 digest as lowercase hex.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString Sha1HashString(const FString& String);
/**
* Returns the SHA-1 digest of a byte array as 40 lowercase hex characters.
* @note For integrity checks and cache keys, not for security or password storage.
* @param Bytes - The bytes to hash.
* @returns The SHA-1 digest as lowercase hex.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString Sha1HashBytes(const TArray<uint8>& Bytes);
/**
* Returns the CRC32 checksum of the string (seed 0). The string is converted to
* UTF-8 bytes first.
* @note For integrity checks and cache keys, not for security or password storage.
* @param String - The string to checksum.
* @returns The CRC32 checksum.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static int32 Crc32String(const FString& String);
/**
* Returns the CRC32 checksum of a byte array (seed 0).
* @note For integrity checks and cache keys, not for security or password storage.
* @param Bytes - The bytes to checksum.
* @returns The CRC32 checksum.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static int32 Crc32Bytes(const TArray<uint8>& Bytes);
/**
* Checks whether the string is safe to use as a bare file name: not empty, no path
* separators or relative segments, and no characters invalid in file names.
* @param String - The string to check.
* @returns True if the string is a valid bare file name.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static bool IsValidFileName(const FString& String);
/**
* Returns the string with path separators and characters invalid in file names removed
* (or replaced when a replacement character is provided). May return an empty string.
* @param String - The string to sanitize.
* @param Replacement - Optional single-character replacement for stripped characters.
* @returns The sanitized file name.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString SanitizeFileName(const FString& String, const FString& Replacement = TEXT(""));
/**
* Finds the entry in Candidates most similar to Input (by GetStringSimilarity).
* Ties resolve to the earliest index. Cost grows with array size and string
* lengths (Levenshtein per candidate). Avoid very large arrays per frame.
* @note bCaseSensitive defaults to false (matching user input), unlike the pairwise
* comparison functions where it defaults to true.
* @param Input - The string to match.
* @param Candidates - The candidate strings.
* @param OutSimilarity - [out] The winning similarity in [0, 1], 0 when empty.
* @param bCaseSensitive - Whether comparison is case-sensitive. Defaults to false.
* @returns The index of the best match, or -1 if Candidates is empty.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static int32 FindBestStringMatch(const FString& Input, const TArray<FString>& Candidates, float& OutSimilarity, bool bCaseSensitive = false);
};

View File

@@ -0,0 +1,26 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "DirectiveUtilTextFunctionLibrary.generated.h"
/**
* DirectiveUtilTextFunctionLibrary
* A collection of helpful text utility functions that improve the usability of text in Blueprints.
*/
UCLASS()
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTextFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Returns true if the provided text is not empty.
* @param Text - The text to check.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Text" )
static bool IsNotEmpty(const FText& Text);
};

View File

@@ -0,0 +1,181 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "UObject/SoftObjectPtr.h"
#include "DirectiveUtilTask_AsyncLoadAsset.generated.h"
struct FStreamableHandle;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAsyncLoadAssetCompleted, UObject*, LoadedAsset);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAsyncLoadClassCompleted, UClass*, LoadedClass);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAsyncLoadAssetsCompleted, const TArray<UObject*>&, LoadedAssets);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnAsyncLoadAssetsProgress, int32, LoadedCount, int32, TotalCount);
/**
* DirectiveUtilTask_AsyncLoadAsset
* Asynchronously loads a soft object reference and broadcasts the loaded asset, with a cancel option.
*/
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Load Asset"))
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_AsyncLoadAsset : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
/**
* Asynchronously loads the asset referenced by a soft object pointer.
* The Completed delegate is called with the loaded asset on success; the Failed delegate is called with null on failure. A manual Cancel() does not broadcast Failed.
*
* @param WorldContextObject The world context object.
* @param Asset The soft object reference to load.
*/
UFUNCTION(
BlueprintCallable,
meta=(
BlueprintInternalUseOnly = "true",
Category = "Directive Utilities|AssetManagement",
WorldContext = "WorldContextObject",
DisplayName = "Async Load Asset"
))
static UDirectiveUtilTask_AsyncLoadAsset* AsyncLoadAsset(UObject* WorldContextObject, const TSoftObjectPtr<UObject> Asset);
/**
* Cancels the in-progress load. The Failed delegate is not broadcast for a manual cancel.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|AssetManagement")
void Cancel();
virtual void Activate() override;
// Called when the asset has finished loading. The loaded asset is valid.
UPROPERTY(BlueprintAssignable)
FOnAsyncLoadAssetCompleted Completed;
// Called when the load failed. The loaded asset is null.
UPROPERTY(BlueprintAssignable)
FOnAsyncLoadAssetCompleted Failed;
protected:
TSoftObjectPtr<UObject> SoftAsset;
TSharedPtr<FStreamableHandle> StreamableHandle;
void OnLoaded();
};
/**
* DirectiveUtilTask_AsyncLoadClass
* Asynchronously loads a soft class reference and broadcasts the loaded class, with a cancel option.
*/
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Load Class"))
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_AsyncLoadClass : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
/**
* Asynchronously loads the class referenced by a soft class pointer.
* The Completed delegate is called with the loaded class on success; the Failed delegate is called with null on failure. A manual Cancel() does not broadcast Failed.
*
* @param WorldContextObject The world context object.
* @param AssetClass The soft class reference to load.
*/
UFUNCTION(
BlueprintCallable,
meta=(
BlueprintInternalUseOnly = "true",
Category = "Directive Utilities|AssetManagement",
WorldContext = "WorldContextObject",
DisplayName = "Async Load Class"
))
static UDirectiveUtilTask_AsyncLoadClass* AsyncLoadClass(UObject* WorldContextObject, const TSoftClassPtr<UObject> AssetClass);
/**
* Cancels the in-progress load. The Failed delegate is not broadcast for a manual cancel.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|AssetManagement")
void Cancel();
virtual void Activate() override;
// Called when the class has finished loading. The loaded class is valid.
UPROPERTY(BlueprintAssignable)
FOnAsyncLoadClassCompleted Completed;
// Called when the load failed. The loaded class is null.
UPROPERTY(BlueprintAssignable)
FOnAsyncLoadClassCompleted Failed;
protected:
TSoftClassPtr<UObject> SoftClass;
TSharedPtr<FStreamableHandle> StreamableHandle;
void OnLoaded();
};
/**
* DirectiveUtilTask_AsyncLoadAssets
* Asynchronously loads a batch of soft object references in a single request and broadcasts the
* loaded assets, with progress updates and a cancel option.
*/
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Load Assets"))
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_AsyncLoadAssets : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
/**
* Asynchronously loads the assets referenced by an array of soft object pointers in a single request.
* The Completed delegate is called exactly once with the loaded assets in input order; entries that were
* unset or failed to resolve are null. An empty input array completes immediately with an empty array.
* A manual Cancel() does not broadcast Completed.
*
* @param WorldContextObject The world context object.
* @param Assets The soft object references to load.
*/
UFUNCTION(
BlueprintCallable,
meta=(
BlueprintInternalUseOnly = "true",
Category = "Directive Utilities|AssetManagement",
WorldContext = "WorldContextObject",
DisplayName = "Async Load Assets"
))
static UDirectiveUtilTask_AsyncLoadAssets* AsyncLoadAssets(UObject* WorldContextObject, const TArray<TSoftObjectPtr<UObject>>& Assets);
/**
* Cancels the in-progress load. The Completed delegate is not broadcast for a manual cancel.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|AssetManagement")
void Cancel();
virtual void Activate() override;
// Called exactly once when the batch has finished loading. The assets are in input order with null
// entries for references that were unset or failed to resolve, and are only guaranteed alive during
// this broadcast.
UPROPERTY(BlueprintAssignable)
FOnAsyncLoadAssetsCompleted Completed;
// Called as assets arrive, with the number loaded so far and the total requested. Not called when
// the request finishes before the first update (e.g. all assets were already in memory).
UPROPERTY(BlueprintAssignable)
FOnAsyncLoadAssetsProgress Progress;
protected:
TArray<TSoftObjectPtr<UObject>> SoftAssets;
TSharedPtr<FStreamableHandle> StreamableHandle;
/* Whether the task has already completed, guarding against a second broadcast. */
bool bHasCompleted = false;
void OnLoaded();
void OnUpdate(TSharedRef<FStreamableHandle> Handle);
};

View File

@@ -0,0 +1,136 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "Engine/EngineTypes.h"
#include "Engine/HitResult.h"
#include "DirectiveUtilTask_AsyncTrace.generated.h"
struct FTraceHandle;
struct FTraceDatum;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAsyncTraceCompleted, const TArray<FHitResult>&, Hits);
/** The collision shape used by an async trace. */
enum class EDirectiveUtilTraceShape : uint8
{
Line,
Sphere,
Box,
Capsule,
};
/**
* DirectiveUtilTask_AsyncTrace
* Queues a collision trace and broadcasts the hit results on the next tick.
* The trace cannot be cancelled.
*/
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Trace"))
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_AsyncTrace : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
/**
* Performs an asynchronous line trace against the given trace channel.
* @param WorldContextObject The world context object.
* @param Start The start of the trace.
* @param End The end of the trace.
* @param TraceChannel The trace channel to test against.
* @param bMultiTrace If true, returns all hits up to and including the first blocking hit; otherwise returns only the first blocking hit.
*/
UFUNCTION(
BlueprintCallable,
meta=(
BlueprintInternalUseOnly = "true",
Category = "Directive Utilities|Collision",
WorldContext = "WorldContextObject",
DisplayName = "Async Line Trace By Channel"
))
static UDirectiveUtilTask_AsyncTrace* AsyncLineTraceByChannel(UObject* WorldContextObject, const FVector Start, const FVector End, const ETraceTypeQuery TraceChannel, const bool bMultiTrace = false);
/**
* Performs an asynchronous sphere sweep against the given trace channel.
* @param WorldContextObject The world context object.
* @param Start The start of the sweep.
* @param End The end of the sweep.
* @param Radius The radius of the sphere.
* @param TraceChannel The trace channel to test against.
* @param bMultiTrace If true, returns all hits up to and including the first blocking hit; otherwise returns only the first blocking hit.
*/
UFUNCTION(
BlueprintCallable,
meta=(
BlueprintInternalUseOnly = "true",
Category = "Directive Utilities|Collision",
WorldContext = "WorldContextObject",
DisplayName = "Async Sphere Trace By Channel"
))
static UDirectiveUtilTask_AsyncTrace* AsyncSphereTraceByChannel(UObject* WorldContextObject, const FVector Start, const FVector End, const float Radius, const ETraceTypeQuery TraceChannel, const bool bMultiTrace = false);
/**
* Performs an asynchronous box sweep against the given trace channel.
* @param WorldContextObject The world context object.
* @param Start The start of the sweep.
* @param End The end of the sweep.
* @param HalfSize The half-extents of the box.
* @param Orientation The orientation of the box.
* @param TraceChannel The trace channel to test against.
* @param bMultiTrace If true, returns all hits up to and including the first blocking hit; otherwise returns only the first blocking hit.
*/
UFUNCTION(
BlueprintCallable,
meta=(
BlueprintInternalUseOnly = "true",
Category = "Directive Utilities|Collision",
WorldContext = "WorldContextObject",
DisplayName = "Async Box Trace By Channel"
))
static UDirectiveUtilTask_AsyncTrace* AsyncBoxTraceByChannel(UObject* WorldContextObject, const FVector Start, const FVector End, const FVector HalfSize, const FRotator Orientation, const ETraceTypeQuery TraceChannel, const bool bMultiTrace = false);
/**
* Performs an asynchronous capsule sweep against the given trace channel.
* @param WorldContextObject The world context object.
* @param Start The start of the sweep.
* @param End The end of the sweep.
* @param Radius The radius of the capsule.
* @param HalfHeight The half-height of the capsule (including the radius).
* @param TraceChannel The trace channel to test against.
* @param bMultiTrace If true, returns all hits up to and including the first blocking hit; otherwise returns only the first blocking hit.
*/
UFUNCTION(
BlueprintCallable,
meta=(
BlueprintInternalUseOnly = "true",
Category = "Directive Utilities|Collision",
WorldContext = "WorldContextObject",
DisplayName = "Async Capsule Trace By Channel"
))
static UDirectiveUtilTask_AsyncTrace* AsyncCapsuleTraceByChannel(UObject* WorldContextObject, const FVector Start, const FVector End, const float Radius, const float HalfHeight, const ETraceTypeQuery TraceChannel, const bool bMultiTrace = false);
virtual void Activate() override;
// Called when the trace has completed. Empty if nothing was hit.
UPROPERTY(BlueprintAssignable)
FOnAsyncTraceCompleted Completed;
protected:
UPROPERTY()
TObjectPtr<UObject> WorldContextObject;
FVector Start = FVector::ZeroVector;
FVector End = FVector::ZeroVector;
ETraceTypeQuery TraceChannel = ETraceTypeQuery::TraceTypeQuery1;
bool bMultiTrace = false;
EDirectiveUtilTraceShape Shape = EDirectiveUtilTraceShape::Line;
float Radius = 0.0f;
float HalfHeight = 0.0f;
FVector HalfSize = FVector::ZeroVector;
FQuat Orientation = FQuat::Identity;
void OnTraceComplete(const FTraceHandle& Handle, FTraceDatum& Datum);
};

View File

@@ -0,0 +1,61 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "Engine/TimerHandle.h"
#include "DirectiveUtilTask_Delay.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnDelayCompleted);
/**
* DirectiveUtilTask_Delay
* A cancellable delay that can be ended early by calling EndTask.
*/
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Cancellable Delay"))
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_Delay : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
/**
* Starts a cancellable delay.
* When the delay has completed, the Completed delegate is called.
* Call EndTask to cancel the delay before it completes.
*
* @param WorldContextObject The world context object.
* @param Duration The duration of the delay in seconds.
*/
UFUNCTION(
BlueprintCallable,
meta=(
BlueprintInternalUseOnly = "true",
Category = "Directive Utilities|FlowControl",
WorldContext = "WorldContextObject",
DisplayName = "Cancellable Delay"
))
static UDirectiveUtilTask_Delay* CancellableDelay(UObject* WorldContextObject, float Duration);
/**
* Ends the delay early.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|FlowControl")
void EndTask();
virtual void Activate() override;
// The delegate called when the delay has completed.
UPROPERTY(BlueprintAssignable)
FOnDelayCompleted Completed;
protected:
UPROPERTY()
TObjectPtr<UObject> WorldContextObject;
float Duration = 0.0f;
FTimerHandle TimerHandle;
void OnDelayComplete();
};

View File

@@ -0,0 +1,181 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "GameFramework/Controller.h"
#include "DirectiveUtilTask_MoveToLocation.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAsyncMoveToLocation, bool, bSuccess);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAsyncMoveToActor, bool, bSuccess);
/**
* DirectiveUtilTask_MoveToLocation
* Asynchronously moves an actor to a location.
*/
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Move To Location"))
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_MoveToLocation : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
/**
* Moves the actor to the specified location.
* When the movement has succeeded or failed, the Completed delegate is called exactly once with success/failure.
* bSuccess is true only when the pawn ends within AcceptanceRadius of Destination; the task also ends
* (with the same distance test) when path-following stops for any reason.
*
* If the controller or pawn is destroyed while moving, the task will automatically end.
* If bCheckStuckMovement is enabled and the controller gets stuck while moving, the task will automatically end.
*
* @param WorldContextObject The world context object.
* @param Controller The controller to move.
* @param Destination The vector location to move to.
* @param AcceptanceRadius The radius around the destination location that is considered acceptable. Be sure to set this to a reasonable value as the controller may never reach the exact destination.
* @param bCheckStuckMovement Check if the controller gets stuck while moving.
* @param StuckThreshold The distance threshold to consider the controller stuck.
* @param bDebugLineTrace Display a line trace to the destination location for a short duration.
*/
UFUNCTION(
BlueprintCallable,
meta=(
BlueprintInternalUseOnly = "true",
Category = "Directive Utilities|Navigation",
WorldContext = "WorldContextObject",
DisplayName = "Async Move To Location",
AdvancedDisplay=6
))
static UDirectiveUtilTask_MoveToLocation* MoveToLocation(
UObject* WorldContextObject,
AController* Controller,
FVector Destination,
float AcceptanceRadius = 100.0f,
bool bCheckStuckMovement = true,
float StuckThreshold = 1.0f,
bool bDebugLineTrace = false);
/**
* Ends the async action.
* This must be called manually when the task is no longer necessary.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Navigation")
void EndTask();
virtual void Activate() override;
// The delegate called when the movement has completed regardless of success. Fires exactly once.
UPROPERTY(BlueprintAssignable)
FOnAsyncMoveToLocation Completed;
protected:
UPROPERTY()
AController* Controller;
FVector Destination;
FVector StartLocation;
FVector CurrentLocation;
FVector LastCheckedLocation;
float AcceptanceRadius = 10.0f;
bool bCheckStuckMovement = true;
float StuckThreshold = 1.0f;
bool bDebugLineTrace;
FTimerHandle TimerHandle;
FTimerHandle StuckTimerHandle;
bool bHasCompleted = false;
void CheckMoveToLocation();
void CheckStuckMovement();
virtual void ExecuteCompleted(bool bSuccess);
};
/**
* DirectiveUtilTask_MoveToActor
* Asynchronously moves an actor to another actor.
*/
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Move To Actor"))
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_MoveToActor : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
/**
* Moves the controller's pawn to the goal actor.
* When the movement has succeeded or failed, the Completed delegate is called exactly once with success/failure.
* bSuccess is true only when the pawn ends within AcceptanceRadius of the goal actor; the task also ends
* (with the same distance test) when path-following stops for any reason. The goal's location is re-read
* every poll, so a moving goal is tracked.
*
* If the controller, pawn, or goal actor is destroyed while moving, the task will automatically end.
* If bCheckStuckMovement is enabled and the controller gets stuck while moving, the task will automatically end.
*
* @param WorldContextObject The world context object.
* @param Controller The controller to move.
* @param Goal The actor to move to.
* @param AcceptanceRadius The radius around the goal actor that is considered acceptable. Be sure to set this to a reasonable value as the controller may never reach the goal's exact location.
* @param bCheckStuckMovement Check if the controller gets stuck while moving.
* @param StuckThreshold The distance threshold to consider the controller stuck.
*/
UFUNCTION(
BlueprintCallable,
meta=(
BlueprintInternalUseOnly = "true",
Category = "Directive Utilities|Navigation",
WorldContext = "WorldContextObject",
DisplayName = "Async Move To Actor"
))
static UDirectiveUtilTask_MoveToActor* MoveToActor(
UObject* WorldContextObject,
AController* Controller,
AActor* Goal,
float AcceptanceRadius = 100.0f,
bool bCheckStuckMovement = true,
float StuckThreshold = 1.0f);
/**
* Ends the async action.
* This must be called manually when the task is no longer necessary.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Navigation")
void EndTask();
virtual void Activate() override;
// The delegate called when the movement has completed regardless of success. Fires exactly once.
UPROPERTY(BlueprintAssignable)
FOnAsyncMoveToActor Completed;
protected:
UPROPERTY()
AController* Controller;
// The cached goal actor; GC nulls it if the actor is destroyed.
UPROPERTY()
TObjectPtr<AActor> Goal;
FVector StartLocation;
FVector CurrentLocation;
FVector LastCheckedLocation;
float AcceptanceRadius = 10.0f;
bool bCheckStuckMovement = true;
float StuckThreshold = 1.0f;
FTimerHandle TimerHandle;
FTimerHandle StuckTimerHandle;
bool bHasCompleted = false;
void CheckMoveToActor();
void CheckStuckMovement();
virtual void ExecuteCompleted(bool bSuccess);
};

View File

@@ -0,0 +1,26 @@
#pragma once
#include "CoreMinimal.h"
#include "DirectiveUtilInputTypes.generated.h"
class UInputMappingContext;
/** An input mapping context and its application priority. */
USTRUCT(BlueprintType)
struct FDirectiveUtilEnhancedInputContextData
{
GENERATED_BODY()
/** The input context to be used. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
TSoftObjectPtr<UInputMappingContext> InputContext;
/** The priority of the input context. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
int32 Priority;
FDirectiveUtilEnhancedInputContextData()
: Priority(0)
{
}
};

View File

@@ -0,0 +1,23 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "DirectiveUtilMathTypes.generated.h"
/**
* Easing curves not provided by the engine's built-in Ease node (EEasingFunc): the classic Penner Back, Elastic and Bounce curves.
*/
UENUM(BlueprintType)
enum class EDirectiveUtilEaseType : uint8
{
BackIn UMETA(DisplayName = "Back In", Tooltip="Overshoots slightly at the start before easing in."),
BackOut UMETA(DisplayName = "Back Out", Tooltip="Overshoots slightly past the end before settling."),
BackInOut UMETA(DisplayName = "Back In Out", Tooltip="Overshoots at both the start and the end."),
ElasticIn UMETA(DisplayName = "Elastic In", Tooltip="Oscillates with increasing amplitude before easing in."),
ElasticOut UMETA(DisplayName = "Elastic Out", Tooltip="Oscillates with decreasing amplitude after the end."),
ElasticInOut UMETA(DisplayName = "Elastic In Out", Tooltip="Oscillates at both the start and the end."),
BounceIn UMETA(DisplayName = "Bounce In", Tooltip="Bounces with increasing energy before easing in."),
BounceOut UMETA(DisplayName = "Bounce Out", Tooltip="Bounces with decreasing energy after the end."),
BounceInOut UMETA(DisplayName = "Bounce In Out", Tooltip="Bounces at both the start and the end."),
};

View File

@@ -0,0 +1,14 @@
#pragma once
#include "CoreMinimal.h"
#include "DirectiveUtilTypes.generated.h"
/**
* Provides a list of success types.
*/
UENUM(BlueprintType)
enum class EDirectiveUtilSuccessStatus : uint8
{
Success,
Failure,
};

View File

@@ -0,0 +1,35 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
using UnrealBuildTool;
public class DirectiveUtilitiesTests : ModuleRules
{
public DirectiveUtilitiesTests(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"CoreUObject",
"Engine",
"DirectiveUtilitiesRuntime",
"AutomationTest",
"EnhancedInput",
"GameplayTags"
}
);
if (Target.bBuildEditor)
{
PrivateDependencyModuleNames.Add("DirectiveUtilitiesEditor");
}
}
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "DirectiveUtilitiesTests.h"
void FDirectiveUtilitiesTestsModule::StartupModule()
{
}
void FDirectiveUtilitiesTestsModule::ShutdownModule()
{
}
IMPLEMENT_MODULE(FDirectiveUtilitiesTestsModule, DirectiveUtilitiesTests)

View File

@@ -0,0 +1,211 @@
#include "Libraries/DirectiveUtilArrayFunctionLibrary.h"
#include "Tests/DirectiveUtilTestObject.h"
#include "Misc/AutomationTest.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilArrayFunctionLibraryTest, "DirectiveUtilities.ArrayFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilArrayFunctionLibraryTest::RunTest(const FString& Parameters)
{
UDirectiveUtilTestObject* TestObject = NewObject<UDirectiveUtilTestObject>();
TestObject->TestArray = {1, 2, 3, 4, 5};
FArrayProperty* ArrayProperty = FindFProperty<FArrayProperty>(UDirectiveUtilTestObject::StaticClass(), GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestArray));
const int32 NextIndex = UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, 2, false);
const int32 PreviousIndex = UDirectiveUtilArrayFunctionLibrary::GenericArray_PreviousIndex(&TestObject->TestArray, ArrayProperty, 2, false);
TestEqual("Array_NextIndex should return the next index in the array", NextIndex, 3);
TestEqual("Array_PreviousIndex should return the previous index in the array", PreviousIndex, 1);
const int32 NextIndexLooped = UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, 4, true);
const int32 PreviousIndexLooped = UDirectiveUtilArrayFunctionLibrary::GenericArray_PreviousIndex(&TestObject->TestArray, ArrayProperty, 0, true);
TestEqual("Array_NextIndex should return the first index when looping", NextIndexLooped, 0);
TestEqual("Array_PreviousIndex should return the last index when looping", PreviousIndexLooped, 4);
const int32 NextIndexNonLooped = UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, 4, false);
const int32 PreviousIndexNonLooped = UDirectiveUtilArrayFunctionLibrary::GenericArray_PreviousIndex(&TestObject->TestArray, ArrayProperty, 0, false);
TestEqual("Array_NextIndex should return the last index when not looping", NextIndexNonLooped, 4);
TestEqual("Array_PreviousIndex should return the first index when not looping", PreviousIndexNonLooped, 0);
const int32 NextIndexOutOfBounds = UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, 5, false);
const int32 PreviousIndexOutOfBounds = UDirectiveUtilArrayFunctionLibrary::GenericArray_PreviousIndex(&TestObject->TestArray, ArrayProperty, -1, false);
TestEqual("Array_NextIndex should return the last index when out of bounds", NextIndexOutOfBounds, 4);
TestEqual("Array_PreviousIndex should return the first index when out of bounds", PreviousIndexOutOfBounds, 0);
TestEqual("Array_NextIndex should clamp a negative input index to 0 (no loop)",
UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, -2, false), 0);
TestEqual("Array_NextIndex should clamp a negative input index to 0 (loop)",
UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, -2, true), 0);
TestEqual("Array_NextIndex should wrap a large input index to 0 when looping",
UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, 10, true), 0);
TestEqual("Array_NextIndex should clamp a large input index to the last index when not looping",
UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(&TestObject->TestArray, ArrayProperty, 10, false), 4);
TestEqual("Array_PreviousIndex should clamp a large input index to the last index (no loop)",
UDirectiveUtilArrayFunctionLibrary::GenericArray_PreviousIndex(&TestObject->TestArray, ArrayProperty, 10, false), 4);
TestEqual("Array_PreviousIndex should clamp a large input index to the last index (loop)",
UDirectiveUtilArrayFunctionLibrary::GenericArray_PreviousIndex(&TestObject->TestArray, ArrayProperty, 10, true), 4);
TestEqual("Array_PreviousIndex should wrap a negative input index to the last index when looping",
UDirectiveUtilArrayFunctionLibrary::GenericArray_PreviousIndex(&TestObject->TestArray, ArrayProperty, -1, true), 4);
TestObject->TestArray = {1, 2, 3, 4, 5};
int32 FirstItem = -1;
const bool bGotFirst = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetFirstItem(&TestObject->TestArray, ArrayProperty, &FirstItem);
TestTrue("GetFirstItem should succeed on a non-empty array", bGotFirst);
TestEqual("GetFirstItem should return the first element", FirstItem, 1);
int32 LastItem = -1;
const bool bGotLast = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetLastItem(&TestObject->TestArray, ArrayProperty, &LastItem);
TestTrue("GetLastItem should succeed on a non-empty array", bGotLast);
TestEqual("GetLastItem should return the last element", LastItem, 5);
int32 IndexItem = -1;
const bool bGotIndex = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetItemAtIndex(&TestObject->TestArray, ArrayProperty, 2, &IndexItem);
TestTrue("GetItemAtIndex should succeed for a valid index", bGotIndex);
TestEqual("GetItemAtIndex should return the element at the index", IndexItem, 3);
int32 OutOfRangeItem = 777;
const bool bGotOutOfRange = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetItemAtIndex(&TestObject->TestArray, ArrayProperty, 10, &OutOfRangeItem);
TestFalse("GetItemAtIndex should fail for an out-of-range index", bGotOutOfRange);
TestEqual("GetItemAtIndex should reset the output to default on failure", OutOfRangeItem, 0);
int32 RandomItem = -1;
int32 RandomIndex = -1;
const bool bGotRandom = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetRandomItem(&TestObject->TestArray, ArrayProperty, &RandomItem, &RandomIndex);
TestTrue("GetRandomItem should succeed on a non-empty array", bGotRandom);
TestTrue("GetRandomItem should return a valid index", TestObject->TestArray.IsValidIndex(RandomIndex));
if (TestObject->TestArray.IsValidIndex(RandomIndex))
{
TestEqual("GetRandomItem item should match the element at the returned index", RandomItem, TestObject->TestArray[RandomIndex]);
}
TestObject->TestArray.Empty();
int32 EmptyItem = 999;
TestFalse("GetFirstItem should fail on an empty array", UDirectiveUtilArrayFunctionLibrary::GenericArray_GetFirstItem(&TestObject->TestArray, ArrayProperty, &EmptyItem));
TestEqual("GetFirstItem should reset the output to default on an empty array", EmptyItem, 0);
EmptyItem = 999;
TestFalse("GetLastItem should fail on an empty array", UDirectiveUtilArrayFunctionLibrary::GenericArray_GetLastItem(&TestObject->TestArray, ArrayProperty, &EmptyItem));
TestEqual("GetLastItem should reset the output to default on an empty array", EmptyItem, 0);
int32 EmptyRandomItem = 999;
int32 EmptyRandomIndex = 5;
TestFalse("GetRandomItem should fail on an empty array", UDirectiveUtilArrayFunctionLibrary::GenericArray_GetRandomItem(&TestObject->TestArray, ArrayProperty, &EmptyRandomItem, &EmptyRandomIndex));
TestEqual("GetRandomItem should return INDEX_NONE on an empty array", EmptyRandomIndex, static_cast<int32>(INDEX_NONE));
TestEqual("GetRandomItem should reset the output to default on an empty array", EmptyRandomItem, 0);
TestObject->TestArray = {1, 2, 3};
int32 PoppedItem = -1;
const bool bPopped = UDirectiveUtilArrayFunctionLibrary::GenericArray_Pop(&TestObject->TestArray, ArrayProperty, &PoppedItem);
TestTrue("Pop should succeed on a non-empty array", bPopped);
TestEqual("Pop should return the last element", PoppedItem, 3);
TestEqual("Pop should shrink the array by one", TestObject->TestArray.Num(), 2);
TestEqual("Pop should leave the new last element intact", TestObject->TestArray.Last(), 2);
TestObject->TestArray = {1, 2, 3};
int32 PoppedFirst = -1;
const bool bPoppedFirst = UDirectiveUtilArrayFunctionLibrary::GenericArray_PopFirst(&TestObject->TestArray, ArrayProperty, &PoppedFirst);
TestTrue("PopFirst should succeed on a non-empty array", bPoppedFirst);
TestEqual("PopFirst should return the first element", PoppedFirst, 1);
TestEqual("PopFirst should shrink the array by one", TestObject->TestArray.Num(), 2);
TestEqual("PopFirst should shift the remaining elements down", TestObject->TestArray[0], 2);
TestObject->TestArray.Empty();
int32 PoppedEmpty = 888;
TestFalse("Pop should fail on an empty array", UDirectiveUtilArrayFunctionLibrary::GenericArray_Pop(&TestObject->TestArray, ArrayProperty, &PoppedEmpty));
TestEqual("Pop should reset the output to default on an empty array", PoppedEmpty, 0);
TestFalse("PopFirst should fail on an empty array", UDirectiveUtilArrayFunctionLibrary::GenericArray_PopFirst(&TestObject->TestArray, ArrayProperty, &PoppedEmpty));
TestObject->TestArray = {10, 20, 30, 40};
const bool bRemoved = UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtSwap(&TestObject->TestArray, ArrayProperty, 1);
TestTrue("RemoveAtSwap should succeed for a valid index", bRemoved);
TestEqual("RemoveAtSwap should shrink the array by one", TestObject->TestArray.Num(), 3);
TestFalse("RemoveAtSwap should have removed the target element", TestObject->TestArray.Contains(20));
TestEqual("RemoveAtSwap should move the previously-last element into the removed slot", TestObject->TestArray[1], 40);
const bool bRemovedOutOfRange = UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtSwap(&TestObject->TestArray, ArrayProperty, 99);
TestFalse("RemoveAtSwap should fail for an out-of-range index", bRemovedOutOfRange);
TestEqual("RemoveAtSwap should not change the array on failure", TestObject->TestArray.Num(), 3);
TestObject->TestArray = {10, 20, 30};
const bool bRemovedLast = UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtSwap(&TestObject->TestArray, ArrayProperty, 2);
TestTrue("RemoveAtSwap should succeed when removing the last element", bRemovedLast);
TestEqual("RemoveAtSwap on the last element should shrink the array", TestObject->TestArray.Num(), 2);
TestEqual("RemoveAtSwap on the last element should preserve the order of the rest", TestObject->TestArray[1], 20);
TestObject->TestArray = {1, 2, 2, 3, 1, 4};
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveDuplicates(&TestObject->TestArray, ArrayProperty);
TestEqual("RemoveDuplicates should remove all duplicate entries", TestObject->TestArray.Num(), 4);
if (TestObject->TestArray.Num() == 4)
{
TestEqual("RemoveDuplicates should keep the first occurrence (index 0)", TestObject->TestArray[0], 1);
TestEqual("RemoveDuplicates should keep the first occurrence (index 1)", TestObject->TestArray[1], 2);
TestEqual("RemoveDuplicates should keep the first occurrence (index 2)", TestObject->TestArray[2], 3);
TestEqual("RemoveDuplicates should keep the first occurrence (index 3)", TestObject->TestArray[3], 4);
}
TestObject->TestArray = {5, 5, 5};
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveDuplicates(&TestObject->TestArray, ArrayProperty);
TestEqual("RemoveDuplicates should collapse an all-duplicates array to one element", TestObject->TestArray.Num(), 1);
if (TestObject->TestArray.Num() == 1)
{
TestEqual("RemoveDuplicates should keep the single remaining value", TestObject->TestArray[0], 5);
}
TestObject->TestArray.Empty();
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveDuplicates(&TestObject->TestArray, ArrayProperty);
TestEqual("RemoveDuplicates on an empty array should leave it empty", TestObject->TestArray.Num(), 0);
TestObject->TestArray = {10, 20, 30, 40, 50};
TArray<int32> SliceOut;
UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(&TestObject->TestArray, ArrayProperty, 1, 3, &SliceOut, ArrayProperty);
TestEqual("Slice should copy a contiguous range", SliceOut, TArray<int32>({20, 30, 40}));
UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(&TestObject->TestArray, ArrayProperty, 3, 99, &SliceOut, ArrayProperty);
TestEqual("Slice should clamp Count to the available elements", SliceOut, TArray<int32>({40, 50}));
UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(&TestObject->TestArray, ArrayProperty, -5, 2, &SliceOut, ArrayProperty);
TestEqual("Slice should clamp a negative start index to 0", SliceOut, TArray<int32>({10, 20}));
UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(&TestObject->TestArray, ArrayProperty, 0, 0, &SliceOut, ArrayProperty);
TestEqual("Slice with Count 0 should be empty", SliceOut.Num(), 0);
TestObject->TestArray = {1, 2, 3, 4, 5};
UDirectiveUtilArrayFunctionLibrary::GenericArray_Rotate(&TestObject->TestArray, ArrayProperty, 2);
TestEqual("Rotate by +2 should rotate toward the end", TestObject->TestArray, TArray<int32>({4, 5, 1, 2, 3}));
TestObject->TestArray = {1, 2, 3, 4, 5};
UDirectiveUtilArrayFunctionLibrary::GenericArray_Rotate(&TestObject->TestArray, ArrayProperty, -1);
TestEqual("Rotate by -1 should rotate toward the start", TestObject->TestArray, TArray<int32>({2, 3, 4, 5, 1}));
TestObject->TestArray = {1, 2, 3};
UDirectiveUtilArrayFunctionLibrary::GenericArray_Rotate(&TestObject->TestArray, ArrayProperty, 3);
TestEqual("Rotate by Length should be a no-op", TestObject->TestArray, TArray<int32>({1, 2, 3}));
TestObject->TestArray = {1, 2, 2, 3, 1, 4};
TArray<int32> DistinctOut;
UDirectiveUtilArrayFunctionLibrary::GenericArray_GetDistinct(&TestObject->TestArray, ArrayProperty, &DistinctOut, ArrayProperty);
TestEqual("GetDistinct should keep first occurrences in order", DistinctOut, TArray<int32>({1, 2, 3, 4}));
TestEqual("GetDistinct should not modify the source array", TestObject->TestArray.Num(), 6);
TestObject->TestArray = {5, 1, 5, 2, 5, 3};
int32 ItemToCount = 5;
TestEqual("CountOccurrences should count matches", UDirectiveUtilArrayFunctionLibrary::GenericArray_CountOccurrences(&TestObject->TestArray, ArrayProperty, &ItemToCount), 3);
int32 MissingItem = 99;
TestEqual("CountOccurrences should return 0 for an absent item", UDirectiveUtilArrayFunctionLibrary::GenericArray_CountOccurrences(&TestObject->TestArray, ArrayProperty, &MissingItem), 0);
TestObject->TestArray = {7, 7, 8, 7, 9, 8};
int32 MostCommonItem = -1;
int32 MostCommonCount = -1;
const bool bGotMostCommon = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetMostCommon(&TestObject->TestArray, ArrayProperty, &MostCommonItem, &MostCommonCount);
TestTrue("GetMostCommon should succeed on a non-empty array", bGotMostCommon);
TestEqual("GetMostCommon should return the most frequent element", MostCommonItem, 7);
TestEqual("GetMostCommon should return the occurrence count", MostCommonCount, 3);
TestObject->TestArray.Empty();
int32 EmptyMostItem = 5;
int32 EmptyMostCount = 5;
TestFalse("GetMostCommon should fail on an empty array", UDirectiveUtilArrayFunctionLibrary::GenericArray_GetMostCommon(&TestObject->TestArray, ArrayProperty, &EmptyMostItem, &EmptyMostCount));
TestEqual("GetMostCommon should reset the count on an empty array", EmptyMostCount, 0);
return true;
}

View File

@@ -0,0 +1,411 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Tasks/DirectiveUtilTask_Delay.h"
#include "Tasks/DirectiveUtilTask_AsyncLoadAsset.h"
#include "Tests/DirectiveUtilTestObject.h"
#include "Engine/World.h"
#include "Engine/Engine.h"
#include "Engine/StaticMesh.h"
#include "Engine/StaticMeshActor.h"
#include "TimerManager.h"
#include "UObject/SoftObjectPath.h"
#include "UObject/SoftObjectPtr.h"
#include "Misc/AutomationTest.h"
#if WITH_EDITOR
namespace DirectiveUtilAsyncTaskTestHelpers
{
/**
* Spawns a transient world, starts a cancellable delay in it, and returns a rooted listener bound
* to the delay's Completed delegate. The world is stored on the listener so the latent command can
* tick its timer manager across frames and tear it down once the scenario settles.
*
* A timer set on a never-ticked FTimerManager is queued as pending and only promoted to active on
* the first tick; it fires on a later tick. Because a manager can be ticked at most once per frame,
* driving the timer to completion requires advancing real frames, hence the latent command below.
*/
UDirectiveUtilDelegateListener* StartDelayScenario(float Duration, bool bCancel)
{
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
if (!World)
{
return nullptr;
}
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor);
WorldContext.SetCurrentWorld(World);
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
Listener->ScenarioWorld = World;
UDirectiveUtilTask_Delay* Task = UDirectiveUtilTask_Delay::CancellableDelay(World, Duration);
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnCompleted);
Task->Activate();
if (bCancel)
{
Task->EndTask();
}
return Listener;
}
}
/**
* Latent command that ticks a delay scenario's world each frame until the delay completes or the
* frame budget runs out, then asserts against the expected outcome and tears the world down.
*/
DEFINE_LATENT_AUTOMATION_COMMAND_FOUR_PARAMETER(FDirectiveUtilTickDelayScenario, FAutomationTestBase*, Test, UDirectiveUtilDelegateListener*, Listener, int32, FramesRemaining, bool, bExpectComplete);
bool FDirectiveUtilTickDelayScenario::Update()
{
if (!Listener)
{
return true;
}
UWorld* World = Listener->ScenarioWorld.Get();
if (World)
{
World->GetTimerManager().Tick(0.1f);
}
const bool bBudgetExhausted = (--FramesRemaining <= 0);
if (Listener->bCompleted || bBudgetExhausted)
{
if (bExpectComplete)
{
Test->TestTrue(TEXT("Cancellable delay broadcasts Completed once its timer elapses"), Listener->bCompleted);
Test->TestEqual(TEXT("Cancellable delay broadcasts Completed exactly once"), Listener->CompletedCount, 1);
}
else
{
Test->TestFalse(TEXT("EndTask cancels the delay so Completed never fires"), Listener->bCompleted);
}
if (World)
{
GEngine->DestroyWorldContext(World);
World->DestroyWorld(false);
}
Listener->ScenarioWorld = nullptr;
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
return true;
}
return false;
}
/**
* DirectiveUtilTask_Delay: verifies the timer-backed completion fires, that EndTask cancels it before it fires,
* and that activating with a null world context is guarded rather than crashing.
*/
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilDelayTaskTest, "DirectiveUtilities.AsyncTaskDelayTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilDelayTaskTest::RunTest(const FString& Parameters)
{
// The null-world activation intentionally logs a warning.
AddExpectedMessagePlain(TEXT("Cancellable Delay failed to activate. World is null."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
// Completion path: the delay fires once its timer elapses (driven across frames by the latent command).
if (UDirectiveUtilDelegateListener* Completed = DirectiveUtilAsyncTaskTestHelpers::StartDelayScenario(0.05f, /*bCancel=*/false))
{
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickDelayScenario(this, Completed, 600, /*bExpectComplete=*/true));
}
else
{
AddError(TEXT("Failed to create a transient world for the delay completion scenario."));
}
// Cancellation path: EndTask clears the timer so Completed never fires across a short window.
if (UDirectiveUtilDelegateListener* Cancelled = DirectiveUtilAsyncTaskTestHelpers::StartDelayScenario(0.05f, /*bCancel=*/true))
{
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickDelayScenario(this, Cancelled, 10, /*bExpectComplete=*/false));
}
else
{
AddError(TEXT("Failed to create a transient world for the delay cancellation scenario."));
}
// Null world guard: activating with a null context object logs a warning and does not crash.
UDirectiveUtilTask_Delay* NullWorldTask = UDirectiveUtilTask_Delay::CancellableDelay(nullptr, 0.05f);
if (TestNotNull("CancellableDelay returns a task even with a null context", NullWorldTask))
{
NullWorldTask->AddToRoot();
NullWorldTask->Activate();
NullWorldTask->RemoveFromRoot();
}
return true;
}
/** Latent command that waits for an async-load listener to settle (completed or failed) or times out. */
DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FDirectiveUtilWaitForAsyncLoad, FAutomationTestBase*, Test, UDirectiveUtilDelegateListener*, Listener, int32, FramesRemaining);
bool FDirectiveUtilWaitForAsyncLoad::Update()
{
if (!Listener)
{
return true;
}
if (Listener->bFailed)
{
Test->AddError(TEXT("Async Load Asset reported failure while loading a valid engine asset."));
Listener->RemoveFromRoot();
Listener->Keepalive = nullptr;
return true;
}
if (Listener->bCompleted)
{
Test->TestNotNull(TEXT("Async Load Asset resolved the requested asset"), Listener->LastObject.Get());
Listener->RemoveFromRoot();
Listener->Keepalive = nullptr;
return true;
}
if (--FramesRemaining <= 0)
{
Test->AddError(TEXT("Async Load Asset did not complete within the frame budget."));
Listener->RemoveFromRoot();
Listener->Keepalive = nullptr;
return true;
}
return false;
}
/**
* DirectiveUtilTask_AsyncLoadAsset: verifies the null soft-reference path broadcasts Failed synchronously, and
* that loading a real engine asset eventually broadcasts Completed with the resolved object.
*/
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilAsyncLoadAssetTest, "DirectiveUtilities.AsyncTaskLoadAssetTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilAsyncLoadAssetTest::RunTest(const FString& Parameters)
{
// The null soft-reference path intentionally logs a warning.
AddExpectedMessagePlain(TEXT("Async Load Asset failed to activate. The soft object reference is null."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
// Failure path: a null soft reference broadcasts Failed synchronously on activation.
{
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_AsyncLoadAsset* Task = UDirectiveUtilTask_AsyncLoadAsset::AsyncLoadAsset(nullptr, TSoftObjectPtr<UObject>());
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnObjectCompleted);
Task->Failed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnObjectFailed);
Task->Activate();
TestTrue("Null soft reference broadcasts Failed", Listener->bFailed);
TestFalse("Null soft reference does not broadcast Completed", Listener->bCompleted);
Listener->RemoveFromRoot();
Listener->Keepalive = nullptr;
}
// Success path: loading a real engine asset broadcasts Completed with the resolved object.
{
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
const TSoftObjectPtr<UObject> SoftCube(FSoftObjectPath(TEXT("/Engine/BasicShapes/Cube.Cube")));
UDirectiveUtilTask_AsyncLoadAsset* Task = UDirectiveUtilTask_AsyncLoadAsset::AsyncLoadAsset(nullptr, SoftCube);
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnObjectCompleted);
Task->Failed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnObjectFailed);
Task->Activate();
// The streamable completion delegate fires on a later engine tick; poll until it settles.
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilWaitForAsyncLoad(this, Listener, 600));
}
return true;
}
/** Latent command that waits for a batch async-load listener to complete, then asserts the slot contract. */
DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FDirectiveUtilWaitForBatchAsyncLoad, FAutomationTestBase*, Test, UDirectiveUtilDelegateListener*, Listener, int32, FramesRemaining);
bool FDirectiveUtilWaitForBatchAsyncLoad::Update()
{
if (!Listener)
{
return true;
}
if (Listener->bCompleted)
{
Test->TestEqual(TEXT("Async Load Assets broadcasts Completed exactly once"), Listener->CompletedCount, 1);
Test->TestEqual(TEXT("Async Load Assets preserves the input slot count"), Listener->LastObjects.Num(), 3);
if (Listener->LastObjects.Num() == 3)
{
Test->TestNotNull(TEXT("Async Load Assets resolves the first asset"), Listener->LastObjects[0].Get());
Test->TestNotNull(TEXT("Async Load Assets resolves the second asset"), Listener->LastObjects[1].Get());
Test->TestNull(TEXT("Async Load Assets keeps a null slot for an unset reference"), Listener->LastObjects[2].Get());
}
Listener->RemoveFromRoot();
Listener->Keepalive = nullptr;
return true;
}
if (--FramesRemaining <= 0)
{
Test->AddError(TEXT("Async Load Assets did not complete within the frame budget."));
Listener->RemoveFromRoot();
Listener->Keepalive = nullptr;
return true;
}
return false;
}
/** Latent command that waits a fixed frame window and then asserts a cancelled batch load never completed. */
DEFINE_LATENT_AUTOMATION_COMMAND_THREE_PARAMETER(FDirectiveUtilVerifyCancelledBatchLoad, FAutomationTestBase*, Test, UDirectiveUtilDelegateListener*, Listener, int32, FramesRemaining);
bool FDirectiveUtilVerifyCancelledBatchLoad::Update()
{
if (!Listener)
{
return true;
}
if (--FramesRemaining > 0)
{
return false;
}
Test->TestFalse(TEXT("Cancel prevents the batch Completed broadcast"), Listener->bCompleted);
Listener->RemoveFromRoot();
Listener->Keepalive = nullptr;
return true;
}
/**
* DirectiveUtilTask_AsyncLoadAssets: verifies a batch resolves in input order with null slots for unset references,
* that an empty input completes immediately with an empty array, and that Cancel suppresses Completed.
*/
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilAsyncLoadAssetsTest, "DirectiveUtilities.AsyncTaskLoadAssetsTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilAsyncLoadAssetsTest::RunTest(const FString& Parameters)
{
// Empty input: Completed broadcasts synchronously on activation with an empty array.
{
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_AsyncLoadAssets* Task = UDirectiveUtilTask_AsyncLoadAssets::AsyncLoadAssets(nullptr, TArray<TSoftObjectPtr<UObject>>());
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnObjectsCompleted);
Task->Activate();
TestTrue("Empty batch broadcasts Completed immediately", Listener->bCompleted);
TestEqual("Empty batch broadcasts Completed exactly once", Listener->CompletedCount, 1);
TestEqual("Empty batch reports an empty array", Listener->LastObjects.Num(), 0);
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
}
// Batch path: two valid engine meshes plus an unset reference resolve in input order.
{
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
TArray<TSoftObjectPtr<UObject>> Assets;
Assets.Add(TSoftObjectPtr<UObject>(FSoftObjectPath(TEXT("/Engine/BasicShapes/Cube.Cube"))));
Assets.Add(TSoftObjectPtr<UObject>(FSoftObjectPath(TEXT("/Engine/BasicShapes/Sphere.Sphere"))));
Assets.Add(TSoftObjectPtr<UObject>());
UDirectiveUtilTask_AsyncLoadAssets* Task = UDirectiveUtilTask_AsyncLoadAssets::AsyncLoadAssets(nullptr, Assets);
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnObjectsCompleted);
Task->Activate();
// The streamable completion delegate fires on a later engine tick; poll until it settles.
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilWaitForBatchAsyncLoad(this, Listener, 600));
}
// Cancel path: cancelling before completion suppresses the Completed broadcast. Uses a mesh no
// other test loads so the request is genuinely in flight when Cancel arrives; if the asset is
// already in memory the batch completes synchronously and there is nothing left to cancel.
{
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
TArray<TSoftObjectPtr<UObject>> Assets;
Assets.Add(TSoftObjectPtr<UObject>(FSoftObjectPath(TEXT("/Engine/EngineMeshes/SM_MatPreviewMesh_01.SM_MatPreviewMesh_01"))));
UDirectiveUtilTask_AsyncLoadAssets* Task = UDirectiveUtilTask_AsyncLoadAssets::AsyncLoadAssets(nullptr, Assets);
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnObjectsCompleted);
Task->Activate();
if (Listener->bCompleted)
{
AddInfo(TEXT("Batch load completed synchronously (asset already in memory); skipping the cancel scenario."));
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
}
else
{
Task->Cancel();
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilVerifyCancelledBatchLoad(this, Listener, 10));
}
}
return true;
}
/**
* DirectiveUtilTask_AsyncLoadClass: verifies the null soft-class path broadcasts Failed synchronously, and that
* loading a real class broadcasts Completed with the resolved class.
*/
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilAsyncLoadClassTest, "DirectiveUtilities.AsyncTaskLoadClassTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilAsyncLoadClassTest::RunTest(const FString& Parameters)
{
// The null soft-class path intentionally logs a warning.
AddExpectedMessagePlain(TEXT("Async Load Class failed to activate. The soft class reference is null."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
// Failure path: a null soft class reference broadcasts Failed synchronously on activation.
{
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_AsyncLoadClass* Task = UDirectiveUtilTask_AsyncLoadClass::AsyncLoadClass(nullptr, TSoftClassPtr<UObject>());
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnClassCompleted);
Task->Failed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnClassFailed);
Task->Activate();
TestTrue("Null soft class broadcasts Failed", Listener->bFailed);
TestFalse("Null soft class does not broadcast Completed", Listener->bCompleted);
Listener->RemoveFromRoot();
Listener->Keepalive = nullptr;
}
// Success path: loading a real class broadcasts Completed with the resolved class.
{
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
const TSoftClassPtr<UObject> SoftClass(AStaticMeshActor::StaticClass());
UDirectiveUtilTask_AsyncLoadClass* Task = UDirectiveUtilTask_AsyncLoadClass::AsyncLoadClass(nullptr, SoftClass);
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnClassCompleted);
Task->Failed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnClassFailed);
Task->Activate();
// The streamable completion delegate fires on a later engine tick; poll until it settles.
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilWaitForAsyncLoad(this, Listener, 600));
}
return true;
}
#endif // WITH_EDITOR

View File

@@ -0,0 +1,502 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Tasks/DirectiveUtilTask_AsyncTrace.h"
#include "Tasks/DirectiveUtilTask_MoveToLocation.h"
#include "Tests/DirectiveUtilTestObject.h"
#include "Engine/World.h"
#include "Engine/Engine.h"
#include "Engine/EngineBaseTypes.h"
#include "Engine/EngineTypes.h"
#include "Engine/StaticMesh.h"
#include "Engine/StaticMeshActor.h"
#include "Components/StaticMeshComponent.h"
#include "GameFramework/DefaultPawn.h"
#include "GameFramework/PlayerController.h"
#include "Misc/AutomationTest.h"
#if WITH_EDITOR
namespace DirectiveUtilAsyncTraceTestHelpers
{
/** Creates a transient game world with a physics scene, initialized for play so traces resolve. */
UWorld* CreateTraceWorld()
{
UWorld* World = UWorld::CreateWorld(EWorldType::Game, false);
if (!World)
{
return nullptr;
}
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Game);
WorldContext.SetCurrentWorld(World);
World->InitializeActorsForPlay(FURL());
World->BeginPlay();
return World;
}
/** Spawns a blocking cube actor at the origin so traces have something to hit. */
AStaticMeshActor* SpawnBlockingCube(UWorld* World, UStaticMesh* CubeMesh)
{
AStaticMeshActor* Cube = World->SpawnActor<AStaticMeshActor>(FVector::ZeroVector, FRotator::ZeroRotator);
UStaticMeshComponent* Component = Cube->GetStaticMeshComponent();
Component->SetMobility(EComponentMobility::Movable);
Component->SetStaticMesh(CubeMesh);
Component->SetCollisionProfileName(TEXT("BlockAll"));
Component->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
Component->UpdateCollisionProfile();
return Cube;
}
}
/**
* Latent command that ticks a trace world each frame until every listener has reported completion
* (or the frame budget runs out), asserts the expected outcome, and tears the world down.
*/
class FDirectiveUtilTickTraceWorld : public IAutomationLatentCommand
{
public:
FDirectiveUtilTickTraceWorld(FAutomationTestBase* InTest, UWorld* InWorld, const TArray<UDirectiveUtilDelegateListener*>& InListeners, int32 InFrames)
: Test(InTest)
, World(InWorld)
, Listeners(InListeners)
, FramesRemaining(InFrames)
{
}
virtual bool Update() override
{
if (UWorld* TickWorld = World.Get())
{
TickWorld->Tick(LEVELTICK_All, 0.05f);
}
bool bAllComplete = true;
for (const UDirectiveUtilDelegateListener* Listener : Listeners)
{
if (Listener && !Listener->bCompleted)
{
bAllComplete = false;
break;
}
}
if (bAllComplete || --FramesRemaining <= 0)
{
for (UDirectiveUtilDelegateListener* Listener : Listeners)
{
if (!Listener)
{
continue;
}
Test->TestTrue(TEXT("Async trace broadcasts Completed"), Listener->bCompleted);
Test->TestTrue(TEXT("Async trace through a blocking cube reports a hit"), Listener->HitCount > 0);
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
}
if (UWorld* TearDownWorld = World.Get())
{
GEngine->DestroyWorldContext(TearDownWorld);
TearDownWorld->DestroyWorld(false);
}
return true;
}
return false;
}
private:
FAutomationTestBase* Test;
TWeakObjectPtr<UWorld> World;
TArray<UDirectiveUtilDelegateListener*> Listeners;
int32 FramesRemaining;
};
/**
* DirectiveUtilTask_AsyncTrace: verifies the null-world guard broadcasts an empty result, and that each trace
* shape (line, sphere, box, capsule) resolves against a blocking body and reports a hit.
*/
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilAsyncTraceTest, "DirectiveUtilities.AsyncTaskTraceTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilAsyncTraceTest::RunTest(const FString& Parameters)
{
// The null-world activation intentionally logs a warning.
AddExpectedMessagePlain(TEXT("Async Trace failed to activate. World is null."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
// Null world guard: activating with a null context broadcasts an empty result and does not crash.
{
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_AsyncTrace* Task = UDirectiveUtilTask_AsyncTrace::AsyncLineTraceByChannel(nullptr, FVector::ZeroVector, FVector(0, 0, 100), ETraceTypeQuery::TraceTypeQuery1, false);
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnTraceCompleted);
Task->Activate();
TestTrue("Null world trace still broadcasts Completed", Listener->bCompleted);
TestEqual("Null world trace reports no hits", Listener->HitCount, 0);
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
}
UStaticMesh* CubeMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
if (!CubeMesh)
{
AddInfo(TEXT("Engine cube mesh unavailable; skipping async trace hit scenarios."));
return true;
}
UWorld* World = DirectiveUtilAsyncTraceTestHelpers::CreateTraceWorld();
if (!World)
{
AddError(TEXT("Failed to create a transient game world for the async trace test."));
return false;
}
DirectiveUtilAsyncTraceTestHelpers::SpawnBlockingCube(World, CubeMesh);
// Trace straight down through the cube at the origin so every shape intersects it.
const FVector Start(0.0f, 0.0f, 500.0f);
const FVector End(0.0f, 0.0f, -500.0f);
const ETraceTypeQuery Channel = ETraceTypeQuery::TraceTypeQuery1; // Visibility, which BlockAll blocks.
auto MakeListener = [](UDirectiveUtilTask_AsyncTrace* Task) -> UDirectiveUtilDelegateListener*
{
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnTraceCompleted);
Task->Activate();
return Listener;
};
TArray<UDirectiveUtilDelegateListener*> Listeners;
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncLineTraceByChannel(World, Start, End, Channel, false)));
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncSphereTraceByChannel(World, Start, End, 25.0f, Channel, false)));
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncBoxTraceByChannel(World, Start, End, FVector(25.0f), FRotator::ZeroRotator, Channel, false)));
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncCapsuleTraceByChannel(World, Start, End, 25.0f, 50.0f, Channel, false)));
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickTraceWorld(this, World, Listeners, 120));
return true;
}
/**
* Latent command that ticks a move-to-location world each frame until the listener reports
* completion (or the frame budget runs out), asserts a single failed completion, and tears the
* world down.
*/
class FDirectiveUtilTickMoveToLocationWorld : public IAutomationLatentCommand
{
public:
FDirectiveUtilTickMoveToLocationWorld(FAutomationTestBase* InTest, UWorld* InWorld, UDirectiveUtilDelegateListener* InListener, int32 InFrames)
: Test(InTest)
, World(InWorld)
, Listener(InListener)
, FramesRemaining(InFrames)
{
}
virtual bool Update() override
{
if (UWorld* TickWorld = World.Get())
{
TickWorld->Tick(LEVELTICK_All, 0.05f);
}
if (Listener && !Listener->bCompleted && --FramesRemaining > 0)
{
return false;
}
if (Listener)
{
Test->TestTrue(TEXT("Move without navigation broadcasts Completed"), Listener->bCompleted);
Test->TestFalse(TEXT("Move without navigation reports failure"), Listener->bLastSuccess);
Test->TestEqual(TEXT("Move without navigation completes exactly once"), Listener->CompletedCount, 1);
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
}
if (UWorld* TearDownWorld = World.Get())
{
GEngine->DestroyWorldContext(TearDownWorld);
TearDownWorld->DestroyWorld(false);
}
return true;
}
private:
FAutomationTestBase* Test;
TWeakObjectPtr<UWorld> World;
UDirectiveUtilDelegateListener* Listener;
int32 FramesRemaining;
};
/**
* DirectiveUtilTask_MoveToLocation: verifies the guard paths (null controller, controller without a pawn) and
* EndTask all broadcast Completed(false) without crashing, that a second EndTask does not broadcast
* again, and that a move with no navigation data terminates with failure. The successful navigation
* path requires a built navigation mesh and is exercised in a project-level test rather than here.
*/
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMoveToLocationTest, "DirectiveUtilities.AsyncTaskMoveToLocationTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilMoveToLocationTest::RunTest(const FString& Parameters)
{
// The guard paths intentionally log a warning.
AddExpectedMessagePlain(TEXT("Controller or pawn has been destroyed while moving to location. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
// Null controller guard: activating broadcasts Completed(false).
{
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_MoveToLocation* Task = UDirectiveUtilTask_MoveToLocation::MoveToLocation(nullptr, nullptr, FVector(100.0f, 0.0f, 0.0f));
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
Task->Activate();
TestTrue("Null controller broadcasts Completed", Listener->bCompleted);
TestFalse("Null controller reports failure", Listener->bLastSuccess);
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
}
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
if (!World)
{
AddError(TEXT("Failed to create a transient world for the move-to-location test."));
return false;
}
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor);
WorldContext.SetCurrentWorld(World);
// Controller-without-pawn guard: activating broadcasts Completed(false).
{
APlayerController* Controller = World->SpawnActor<APlayerController>();
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_MoveToLocation* Task = UDirectiveUtilTask_MoveToLocation::MoveToLocation(World, Controller, FVector(100.0f, 0.0f, 0.0f));
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
Task->Activate();
TestTrue("Controller without a pawn broadcasts Completed", Listener->bCompleted);
TestFalse("Controller without a pawn reports failure", Listener->bLastSuccess);
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
}
// EndTask broadcasts Completed(false) and clears timers without crashing.
{
APlayerController* Controller = World->SpawnActor<APlayerController>();
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_MoveToLocation* Task = UDirectiveUtilTask_MoveToLocation::MoveToLocation(World, Controller, FVector(100.0f, 0.0f, 0.0f));
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
Task->EndTask();
TestTrue("EndTask broadcasts Completed", Listener->bCompleted);
TestFalse("EndTask reports failure", Listener->bLastSuccess);
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
}
// Double completion guard: a second EndTask does not broadcast Completed again.
{
APlayerController* Controller = World->SpawnActor<APlayerController>();
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_MoveToLocation* Task = UDirectiveUtilTask_MoveToLocation::MoveToLocation(World, Controller, FVector(100.0f, 0.0f, 0.0f));
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
Task->EndTask();
Task->EndTask();
TestEqual("Double EndTask broadcasts Completed exactly once", Listener->CompletedCount, 1);
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
}
GEngine->DestroyWorldContext(World);
World->DestroyWorld(false);
// No-navigation failure: without a navmesh the idle path-following check terminates the task
// with failure instead of polling forever.
{
// SimpleMoveToLocation may warn when the world has no navigation system.
AddExpectedMessagePlain(TEXT("SimpleMoveToActor called for NavSys:"), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
AddExpectedMessagePlain(TEXT("SimpleMove failed for"), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
UWorld* MoveWorld = DirectiveUtilAsyncTraceTestHelpers::CreateTraceWorld();
if (!MoveWorld)
{
AddInfo(TEXT("Failed to create a transient game world; skipping the no-navigation move scenario."));
return true;
}
APlayerController* Controller = MoveWorld->SpawnActor<APlayerController>();
ADefaultPawn* Pawn = MoveWorld->SpawnActor<ADefaultPawn>(FVector::ZeroVector, FRotator::ZeroRotator);
if (!Controller || !Pawn)
{
AddInfo(TEXT("Failed to spawn a controller or pawn; skipping the no-navigation move scenario."));
GEngine->DestroyWorldContext(MoveWorld);
MoveWorld->DestroyWorld(false);
return true;
}
Controller->SetPawn(Pawn);
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_MoveToLocation* Task = UDirectiveUtilTask_MoveToLocation::MoveToLocation(MoveWorld, Controller, FVector(10000.0f, 0.0f, 0.0f), 100.0f, false);
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
Task->Activate();
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickMoveToLocationWorld(this, MoveWorld, Listener, 120));
}
return true;
}
/**
* DirectiveUtilTask_MoveToActor: verifies the guard paths (null controller, null goal) broadcast Completed(false)
* without crashing, that a second EndTask does not broadcast again, and that a move with no
* navigation data terminates with failure. The successful navigation path requires a built
* navigation mesh and is exercised in a project-level test rather than here.
*/
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMoveToActorTest, "DirectiveUtilities.AsyncTaskMoveToActorTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilMoveToActorTest::RunTest(const FString& Parameters)
{
// The guard paths intentionally log a warning.
AddExpectedMessagePlain(TEXT("Controller, pawn, or goal has been destroyed while moving to actor. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
// Null controller guard: activating broadcasts Completed(false).
{
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_MoveToActor* Task = UDirectiveUtilTask_MoveToActor::MoveToActor(nullptr, nullptr, nullptr);
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
Task->Activate();
TestTrue("Null controller broadcasts Completed", Listener->bCompleted);
TestFalse("Null controller reports failure", Listener->bLastSuccess);
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
}
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
if (!World)
{
AddError(TEXT("Failed to create a transient world for the move-to-actor test."));
return false;
}
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor);
WorldContext.SetCurrentWorld(World);
// Null goal guard: a controller with a pawn but no goal broadcasts Completed(false).
{
APlayerController* Controller = World->SpawnActor<APlayerController>();
ADefaultPawn* Pawn = World->SpawnActor<ADefaultPawn>(FVector::ZeroVector, FRotator::ZeroRotator);
if (Controller && Pawn)
{
Controller->SetPawn(Pawn);
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_MoveToActor* Task = UDirectiveUtilTask_MoveToActor::MoveToActor(World, Controller, nullptr);
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
Task->Activate();
TestTrue("Null goal broadcasts Completed", Listener->bCompleted);
TestFalse("Null goal reports failure", Listener->bLastSuccess);
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
}
else
{
AddInfo(TEXT("Failed to spawn a controller or pawn; skipping the null-goal scenario."));
}
}
// Double completion guard: a second EndTask does not broadcast Completed again.
{
APlayerController* Controller = World->SpawnActor<APlayerController>();
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_MoveToActor* Task = UDirectiveUtilTask_MoveToActor::MoveToActor(World, Controller, nullptr);
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
Task->EndTask();
Task->EndTask();
TestEqual("Double EndTask broadcasts Completed exactly once", Listener->CompletedCount, 1);
Listener->Keepalive = nullptr;
Listener->RemoveFromRoot();
}
GEngine->DestroyWorldContext(World);
World->DestroyWorld(false);
// No-navigation failure: without a navmesh the idle path-following check terminates the task
// with failure instead of polling forever.
{
// SimpleMoveToActor may warn when the world has no navigation system.
AddExpectedMessagePlain(TEXT("SimpleMoveToActor called for NavSys:"), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
AddExpectedMessagePlain(TEXT("SimpleMove failed for"), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
UWorld* MoveWorld = DirectiveUtilAsyncTraceTestHelpers::CreateTraceWorld();
if (!MoveWorld)
{
AddInfo(TEXT("Failed to create a transient game world; skipping the no-navigation move scenario."));
return true;
}
APlayerController* Controller = MoveWorld->SpawnActor<APlayerController>();
ADefaultPawn* Pawn = MoveWorld->SpawnActor<ADefaultPawn>(FVector::ZeroVector, FRotator::ZeroRotator);
AStaticMeshActor* GoalActor = MoveWorld->SpawnActor<AStaticMeshActor>(FVector(10000.0f, 0.0f, 0.0f), FRotator::ZeroRotator);
if (!Controller || !Pawn || !GoalActor)
{
AddInfo(TEXT("Failed to spawn a controller, pawn, or goal; skipping the no-navigation move scenario."));
GEngine->DestroyWorldContext(MoveWorld);
MoveWorld->DestroyWorld(false);
return true;
}
Controller->SetPawn(Pawn);
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
Listener->AddToRoot();
UDirectiveUtilTask_MoveToActor* Task = UDirectiveUtilTask_MoveToActor::MoveToActor(MoveWorld, Controller, GoalActor, 100.0f, false);
Listener->Keepalive = Task;
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
Task->Activate();
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickMoveToLocationWorld(this, MoveWorld, Listener, 120));
}
return true;
}
#endif // WITH_EDITOR

View File

@@ -0,0 +1,52 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Misc/AutomationTest.h"
#include "UObject/UObjectIterator.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilBlueprintCategoryTest, "DirectiveUtilities.BlueprintCategoryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilBlueprintCategoryTest::RunTest(const FString& Parameters)
{
const FString ExpectedCategoryRoot = TEXT("Directive Utilities");
const TSet<FString> PluginScriptPackages = {
TEXT("/Script/DirectiveUtilitiesRuntime"),
TEXT("/Script/DirectiveUtilitiesEditor")
};
int32 TestedFunctionCount = 0;
for (TObjectIterator<UClass> ClassIterator; ClassIterator; ++ClassIterator)
{
UClass* Class = *ClassIterator;
if (!PluginScriptPackages.Contains(Class->GetOutermost()->GetName()))
{
continue;
}
for (TFieldIterator<UFunction> FunctionIterator(Class, EFieldIteratorFlags::ExcludeSuper); FunctionIterator; ++FunctionIterator)
{
const UFunction* Function = *FunctionIterator;
if (!Function->HasAnyFunctionFlags(FUNC_BlueprintCallable | FUNC_BlueprintPure))
{
continue;
}
++TestedFunctionCount;
const FString Category = Function->GetMetaData(TEXT("Category"));
FString CategoryRoot = Category;
int32 CategoryDelimiterIndex = INDEX_NONE;
if (CategoryRoot.FindChar(TEXT('|'), CategoryDelimiterIndex))
{
CategoryRoot.LeftInline(CategoryDelimiterIndex);
}
CategoryRoot.TrimStartAndEndInline();
TestTrue(
FString::Printf(TEXT("%s.%s uses the Directive Utilities category root"), *Class->GetName(), *Function->GetName()),
CategoryRoot == ExpectedCategoryRoot
);
}
}
TestTrue(TEXT("Blueprint-exposed plugin functions were found"), TestedFunctionCount > 0);
return !HasAnyErrors();
}

View File

@@ -0,0 +1,461 @@
#if WITH_EDITOR
#include "Subsystems/DirectiveUtilEditorActorSubsystem.h"
#include "Types/DirectiveUtilEditorTypes.h"
#include "Misc/AutomationTest.h"
#include "Engine/StaticMesh.h"
#include "Engine/StaticMeshActor.h"
#include "Components/StaticMeshComponent.h"
#include "Components/BoxComponent.h"
#include "Components/CapsuleComponent.h"
#include "Materials/MaterialInterface.h"
#include "Engine/World.h"
#include "Engine/Engine.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemTest, "DirectiveUtilities.EditorActorSubsystemTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilEditorActorSubsystemTest::RunTest(const FString& Parameters)
{
// IsActorWithinBoxBounds should not crash and should return false with null Actor
TestFalse("IsActorWithinBoxBounds should return false with null Actor",
UDirectiveUtilEditorActorSubsystem::IsActorWithinBoxBounds(nullptr, nullptr));
// IsActorWithinSphereBounds should not crash and should return false with null Actor
TestFalse("IsActorWithinSphereBounds should return false with null Actor",
UDirectiveUtilEditorActorSubsystem::IsActorWithinSphereBounds(nullptr, nullptr));
// IsActorWithinCapsuleBounds should not crash and should return false with null Actor
TestFalse("IsActorWithinCapsuleBounds should return false with null CapsuleComponent",
UDirectiveUtilEditorActorSubsystem::IsActorWithinCapsuleBounds(nullptr, nullptr));
// FilterEmptyActors should not crash with an empty array
TArray<AActor*> EmptyActors;
TArray<AActor*> FilteredActors;
UDirectiveUtilEditorActorSubsystem::FilterEmptyActors(EmptyActors, FilteredActors, Include);
TestEqual("FilterEmptyActors with empty input should produce empty output", FilteredActors.Num(), 0);
// FilterActorsByMaterialName should not crash with an empty array
TArray<AActor*> MaterialFiltered;
UDirectiveUtilEditorActorSubsystem::FilterActorsByMaterialName(EmptyActors, MaterialFiltered, TEXT("TestMaterial"), OverrideOnly, Include);
TestEqual("FilterActorsByMaterialName with empty input should produce empty output", MaterialFiltered.Num(), 0);
// FilterActorsByVertCount should not crash with an empty array
TArray<AActor*> VertFiltered;
UDirectiveUtilEditorActorSubsystem::FilterActorsByVertCount(EmptyActors, VertFiltered, 0, 1000, Include);
TestEqual("FilterActorsByVertCount with empty input should produce empty output", VertFiltered.Num(), 0);
// FilterActorsByBounds should not crash with an empty array
TArray<AActor*> BoundsFiltered;
UDirectiveUtilEditorActorSubsystem::FilterActorsByBounds(EmptyActors, BoundsFiltered, FVector::ZeroVector, FVector::OneVector, Include);
TestEqual("FilterActorsByBounds with empty input should produce empty output", BoundsFiltered.Num(), 0);
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemFilterTest, "DirectiveUtilities.EditorActorSubsystemFilterTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilEditorActorSubsystemFilterTest::RunTest(const FString& Parameters)
{
UStaticMesh* Cube = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
UStaticMesh* Sphere = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Sphere.Sphere"));
if (!Cube || !Sphere)
{
AddInfo(TEXT("Engine basic shapes unavailable; skipping include/exclude behaviour test."));
return true;
}
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
if (!World)
{
AddError(TEXT("Failed to create a transient world for the filter test."));
return false;
}
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor);
WorldContext.SetCurrentWorld(World);
auto SpawnWithMeshes = [World](const TArray<UStaticMesh*>& Meshes) -> AActor*
{
AActor* Actor = World->SpawnActor<AActor>();
USceneComponent* Root = NewObject<USceneComponent>(Actor);
Actor->SetRootComponent(Root);
Root->RegisterComponent();
for (UStaticMesh* Mesh : Meshes)
{
UStaticMeshComponent* MeshComponent = NewObject<UStaticMeshComponent>(Actor);
MeshComponent->SetupAttachment(Root);
MeshComponent->RegisterComponent();
MeshComponent->SetStaticMesh(Mesh);
Actor->AddInstanceComponent(MeshComponent);
}
return Actor;
};
AActor* ActorCubeAndSphere = SpawnWithMeshes({ Cube, Sphere });
AActor* ActorCubeOnly = SpawnWithMeshes({ Cube });
AActor* ActorNoMesh = SpawnWithMeshes({});
const TArray<AActor*> Source = { ActorCubeAndSphere, ActorCubeOnly, ActorNoMesh };
// Include: actors that contain the cube mesh.
TArray<AActor*> Included;
UDirectiveUtilEditorActorSubsystem::FilterActorsByStaticMesh(Source, Included, Cube, Include);
TestTrue("Include: multi-mesh actor containing the cube is included", Included.Contains(ActorCubeAndSphere));
TestTrue("Include: cube-only actor is included", Included.Contains(ActorCubeOnly));
TestFalse("Include: actor with no mesh is excluded", Included.Contains(ActorNoMesh));
// Exclude: actors that do NOT contain the cube. A multi-mesh actor that uses the cube in one slot
// must still be excluded even though another slot uses a different mesh (the aggregation fix).
TArray<AActor*> Excluded;
UDirectiveUtilEditorActorSubsystem::FilterActorsByStaticMesh(Source, Excluded, Cube, Exclude);
TestFalse("Exclude: multi-mesh actor containing the cube is not mistakenly included", Excluded.Contains(ActorCubeAndSphere));
TestFalse("Exclude: cube-only actor is excluded", Excluded.Contains(ActorCubeOnly));
TestTrue("Exclude: actor with no mesh is included", Excluded.Contains(ActorNoMesh));
GEngine->DestroyWorldContext(World);
World->DestroyWorld(false);
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemFilterCoverageTest, "DirectiveUtilities.EditorActorSubsystemFilterCoverageTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilEditorActorSubsystemFilterCoverageTest::RunTest(const FString& Parameters)
{
UStaticMesh* Cube = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
UStaticMesh* Sphere = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Sphere.Sphere"));
UMaterialInterface* MatA = LoadObject<UMaterialInterface>(nullptr, TEXT("/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial"));
UMaterialInterface* MatB = LoadObject<UMaterialInterface>(nullptr, TEXT("/Engine/EngineMaterials/WorldGridMaterial.WorldGridMaterial"));
if (!Cube || !Sphere || !MatA || !MatB)
{
AddInfo(TEXT("Engine basic shapes/materials unavailable; skipping filter coverage test."));
return true;
}
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
if (!World)
{
AddError(TEXT("Failed to create a transient world for the filter coverage test."));
return false;
}
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor);
WorldContext.SetCurrentWorld(World);
auto SpawnMesh = [World](UStaticMesh* Mesh, EComponentMobility::Type Mobility) -> UStaticMeshComponent*
{
AActor* Actor = World->SpawnActor<AActor>();
USceneComponent* Root = NewObject<USceneComponent>(Actor);
Root->SetMobility(Mobility);
Actor->SetRootComponent(Root);
Root->RegisterComponent();
UStaticMeshComponent* MeshComponent = NewObject<UStaticMeshComponent>(Actor);
MeshComponent->SetMobility(Mobility);
MeshComponent->SetupAttachment(Root);
if (Mesh) { MeshComponent->SetStaticMesh(Mesh); }
MeshComponent->RegisterComponent();
Actor->AddInstanceComponent(MeshComponent);
return MeshComponent;
};
// Actor A: cube, MatA override, Static mobility, BlockAll collision, tag Alpha, at origin.
UStaticMeshComponent* CompA = SpawnMesh(Cube, EComponentMobility::Static);
AActor* ActorA = CompA->GetOwner();
CompA->SetMaterial(0, MatA);
CompA->SetCollisionProfileName(TEXT("BlockAll"));
ActorA->Tags.Add(FName("Alpha"));
ActorA->SetActorLocation(FVector::ZeroVector);
// Actor B: sphere, MatB override, Movable mobility, far away, no tag.
UStaticMeshComponent* CompB = SpawnMesh(Sphere, EComponentMobility::Movable);
AActor* ActorB = CompB->GetOwner();
CompB->SetMaterial(0, MatB);
ActorB->SetActorLocation(FVector(100000.0f, 0.0f, 0.0f));
// Actor C: no static mesh component at all.
AActor* ActorC = World->SpawnActor<AActor>();
USceneComponent* RootC = NewObject<USceneComponent>(ActorC);
ActorC->SetRootComponent(RootC);
RootC->RegisterComponent();
const TArray<AActor*> Src = { ActorA, ActorB, ActorC };
auto RunFilter = [&Src](TFunctionRef<void(const TArray<AActor*>&, TArray<AActor*>&)> Fn) -> TArray<AActor*>
{
TArray<AActor*> Out;
Fn(Src, Out);
return Out;
};
// Class: everything is an AActor.
TestEqual("ByClass(AActor) includes all", RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByClass(S, O, AActor::StaticClass(), Include); }).Num(), 3);
// Tag (Include + Exclude complement).
{
const TArray<AActor*> Inc = RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByTag(S, O, FName("Alpha"), Include); });
const TArray<AActor*> Exc = RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByTag(S, O, FName("Alpha"), Exclude); });
TestTrue("ByTag Include => A only", Inc.Contains(ActorA) && !Inc.Contains(ActorB) && !Inc.Contains(ActorC));
TestTrue("ByTag Exclude => B and C", !Exc.Contains(ActorA) && Exc.Contains(ActorB) && Exc.Contains(ActorC));
}
// Static mesh by reference + by name.
{
const TArray<AActor*> Inc = RunFilter([&](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByStaticMesh(S, O, Cube, Include); });
TestTrue("ByStaticMesh(Cube) => A only", Inc.Contains(ActorA) && !Inc.Contains(ActorB) && !Inc.Contains(ActorC));
const TArray<AActor*> ByName = RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByStaticMeshName(S, O, TEXT("Cube"), Include); });
TestTrue("ByStaticMeshName(Cube) => A", ByName.Contains(ActorA) && !ByName.Contains(ActorB));
}
// Material by reference + by name (override slot).
{
const TArray<AActor*> Inc = RunFilter([&](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByMaterial(S, O, MatA, OverrideOnly, Include); });
const TArray<AActor*> Exc = RunFilter([&](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByMaterial(S, O, MatA, OverrideOnly, Exclude); });
TestTrue("ByMaterial(MatA) Include => A only", Inc.Contains(ActorA) && !Inc.Contains(ActorB));
TestTrue("ByMaterial(MatA) Exclude => B and C, not A", !Exc.Contains(ActorA) && Exc.Contains(ActorB) && Exc.Contains(ActorC));
const TArray<AActor*> ByName = RunFilter([&](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByMaterialName(S, O, MatA->GetName(), OverrideOnly, Include); });
TestTrue("ByMaterialName(MatA) => A", ByName.Contains(ActorA) && !ByName.Contains(ActorB));
}
// Vert / tri count: query the cube's actual counts, then assert in-range includes and out-of-range excludes.
{
const int32 Verts = Cube->GetNumVertices(0);
TestTrue("ByVertCount [V,V] => A", RunFilter([Verts](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByVertCount(S, O, Verts, Verts, Include); }).Contains(ActorA));
TestFalse("ByVertCount out-of-range => not A", RunFilter([Verts](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByVertCount(S, O, Verts + 100000, Verts + 200000, Include); }).Contains(ActorA));
const int32 Tris = Cube->GetNumTriangles(0);
TestTrue("ByTriCount [T,T] => A", RunFilter([Tris](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByTriCount(S, O, Tris, Tris, Include); }).Contains(ActorA));
}
// Mobility.
{
TestTrue("ByMobility(Static) => A, not B", RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByMobility(S, O, EComponentMobility::Static, Include); }).Contains(ActorA));
TestTrue("ByMobility(Movable) => B, not A", RunFilter([&](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByMobility(S, O, EComponentMobility::Movable, Include); }).Contains(ActorB));
}
// Collision (BlockAll on A implies WorldStatic object type, QueryAndPhysics, blocking responses).
{
TestTrue("ByCollisionProfile(BlockAll) => A", RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByCollisionProfile(S, O, FName("BlockAll"), Include); }).Contains(ActorA));
TestTrue("ByCollisionChannel(WorldStatic) => A", RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByCollisionChannel(S, O, ECC_WorldStatic, Include); }).Contains(ActorA));
TestTrue("ByCollisionEnabled(QueryAndPhysics) => A", RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByCollisionEnabled(S, O, ECollisionEnabled::QueryAndPhysics, Include); }).Contains(ActorA));
TestTrue("ByCollisionResponse(WorldDynamic, Block) => A", RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByCollisionResponse(S, O, ECC_WorldDynamic, ECR_Block, Include); }).Contains(ActorA));
}
// Nanite: assert the filter partitions correctly (A matched by exactly one of true/false) without
// reading the deprecated member directly.
{
const bool bInFalse = RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByNaniteState(S, O, false, Include); }).Contains(ActorA);
const bool bInTrue = RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByNaniteState(S, O, true, Include); }).Contains(ActorA);
TestTrue("ByNaniteState partitions A into exactly one of true/false", bInFalse != bInTrue);
}
// LOD count: query the cube's LOD count and assert in-range includes A.
{
const int32 LODs = Cube->GetNumLODs();
TestTrue("ByLODCount [n,n] => A", RunFilter([LODs](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByLODCount(S, O, LODs, LODs, Include); }).Contains(ActorA));
}
// Actor bounds: query A's own size and assert a range around it includes A.
{
FVector Origin, Extent;
ActorA->GetActorBounds(false, Origin, Extent);
const FVector Size = Extent * 2.0f;
TestTrue("ByBounds around A's size => A", RunFilter([&](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByBounds(S, O, Size - FVector(1.0f), Size + FVector(1.0f), Include); }).Contains(ActorA));
}
// World location: A at origin, B far away.
{
const TArray<AActor*> Near = RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByWorldLocation(S, O, FVector::ZeroVector, 50.0f, Include); });
TestTrue("ByWorldLocation near origin => A, not the far B", Near.Contains(ActorA) && !Near.Contains(ActorB));
}
// Texture by name: a name no material uses -> Include matches none, Exclude matches all (exercises traversal).
{
TestEqual("ByTextureName(absent) Include => none", RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByTextureName(S, O, TEXT("__udcore_absent_texture__"), BaseAndOverride, Include); }).Num(), 0);
TestEqual("ByTextureName(absent) Exclude => all", RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByTextureName(S, O, TEXT("__udcore_absent_texture__"), BaseAndOverride, Exclude); }).Num(), 3);
}
GEngine->DestroyWorldContext(World);
World->DestroyWorld(false);
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemEmptyActorsTest, "DirectiveUtilities.EditorActorSubsystemEmptyActorsTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilEditorActorSubsystemEmptyActorsTest::RunTest(const FString& Parameters)
{
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
if (!World)
{
AddError(TEXT("Failed to create a transient world for the empty actors test."));
return false;
}
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor);
WorldContext.SetCurrentWorld(World);
// A bare actor whose only component is a childless scene root: the "Empty Actor" shape.
AActor* BareActor = World->SpawnActor<AActor>();
USceneComponent* BareRoot = NewObject<USceneComponent>(BareActor);
BareActor->SetRootComponent(BareRoot);
BareRoot->RegisterComponent();
AStaticMeshActor* MeshActor = World->SpawnActor<AStaticMeshActor>();
const TArray<AActor*> Source = { BareActor, MeshActor };
TArray<AActor*> Included;
UDirectiveUtilEditorActorSubsystem::FilterEmptyActors(Source, Included, Include);
TestTrue("Include: bare actor with only a scene root is empty", Included.Contains(BareActor));
TestFalse("Include: static mesh actor is not empty", Included.Contains(MeshActor));
TestEqual("Include: only the bare actor is returned", Included.Num(), 1);
TArray<AActor*> Excluded;
UDirectiveUtilEditorActorSubsystem::FilterEmptyActors(Source, Excluded, Exclude);
TestFalse("Exclude: bare actor is not returned", Excluded.Contains(BareActor));
TestTrue("Exclude: static mesh actor is returned", Excluded.Contains(MeshActor));
TestEqual("Exclude: only the static mesh actor is returned", Excluded.Num(), 1);
// Aliasing: filtering an array into itself rebuilds it in place.
TArray<AActor*> Aliased = { BareActor, MeshActor };
UDirectiveUtilEditorActorSubsystem::FilterEmptyActors(Aliased, Aliased, Include);
TestEqual("Aliased: array is rebuilt in place with one entry", Aliased.Num(), 1);
TestTrue("Aliased: only the bare actor remains", Aliased.Contains(BareActor));
GEngine->DestroyWorldContext(World);
World->DestroyWorld(false);
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemQueryAlignmentTest, "DirectiveUtilities.EditorActorSubsystemQueryAlignmentTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilEditorActorSubsystemQueryAlignmentTest::RunTest(const FString& Parameters)
{
UStaticMesh* Cube = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
if (!Cube)
{
AddInfo(TEXT("Engine basic shapes unavailable; skipping query alignment test."));
return true;
}
// The GetActorsBy* queries read from the editor world rather than a passed array.
UWorld* EditorWorld = nullptr;
for (const FWorldContext& Context : GEngine->GetWorldContexts())
{
if (Context.WorldType == EWorldType::Editor && Context.World())
{
EditorWorld = Context.World();
break;
}
}
if (!EditorWorld)
{
AddInfo(TEXT("No editor world available; skipping query alignment test."));
return true;
}
AActor* MeshActor = EditorWorld->SpawnActor<AActor>();
USceneComponent* Root = NewObject<USceneComponent>(MeshActor);
Root->SetMobility(EComponentMobility::Movable);
MeshActor->SetRootComponent(Root);
Root->RegisterComponent();
UStaticMeshComponent* MeshComponent = NewObject<UStaticMeshComponent>(MeshActor);
MeshComponent->SetMobility(EComponentMobility::Movable);
MeshComponent->SetupAttachment(Root);
MeshComponent->SetStaticMesh(Cube);
MeshComponent->RegisterComponent();
MeshActor->AddInstanceComponent(MeshComponent);
MeshActor->SetActorLocation(FVector::ZeroVector);
// The query methods keep no instance state, so a transient instance is enough headless.
UDirectiveUtilEditorActorSubsystem* Subsystem = NewObject<UDirectiveUtilEditorActorSubsystem>();
// Bounding box: an enclosing box finds the actor, a disjoint one does not.
TArray<AActor*> InBox;
Subsystem->GetActorsByBoundingBox(InBox, FVector(-100000.0f), FVector(100000.0f), World, Include);
TestTrue("BoundingBox: enclosing box finds the actor", InBox.Contains(MeshActor));
TArray<AActor*> OutOfBox;
Subsystem->GetActorsByBoundingBox(OutOfBox, FVector(900000.0f), FVector(900100.0f), World, Include);
TestFalse("BoundingBox: disjoint box does not find the actor", OutOfBox.Contains(MeshActor));
// Mesh name: a lowercase substring matches case-insensitively, as in the Filter variant.
TArray<AActor*> ByName;
Subsystem->GetActorsByStaticMeshName(ByName, TEXT("cub"), World, Include);
TestTrue("StaticMeshName: lowercase substring finds the actor", ByName.Contains(MeshActor));
// Mobility: the root component's mobility is what counts.
const TArray<AActor*> Source = { MeshActor };
TArray<AActor*> MovableActors;
UDirectiveUtilEditorActorSubsystem::FilterActorsByMobility(Source, MovableActors, EComponentMobility::Movable, Include);
TestTrue("Mobility: movable root is matched", MovableActors.Contains(MeshActor));
TArray<AActor*> StaticActors;
UDirectiveUtilEditorActorSubsystem::FilterActorsByMobility(Source, StaticActors, EComponentMobility::Static, Include);
TestFalse("Mobility: static does not match a movable root", StaticActors.Contains(MeshActor));
EditorWorld->DestroyActor(MeshActor);
return true;
}
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemBoundsTest, "DirectiveUtilities.EditorActorSubsystemBoundsTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilEditorActorSubsystemBoundsTest::RunTest(const FString& Parameters)
{
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
if (!World)
{
AddError(TEXT("Failed to create a transient world for the bounds test."));
return false;
}
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor);
WorldContext.SetCurrentWorld(World);
auto SpawnPoint = [World](const FVector& Location, const FVector& Scale) -> AActor*
{
AActor* Actor = World->SpawnActor<AActor>();
USceneComponent* Root = NewObject<USceneComponent>(Actor);
Actor->SetRootComponent(Root);
Root->RegisterComponent();
Actor->SetActorScale3D(Scale);
Actor->SetActorLocation(Location);
return Actor;
};
// Axis-aligned box of extent 100 at the origin.
AActor* BoxActor = World->SpawnActor<AActor>();
USceneComponent* BoxRoot = NewObject<USceneComponent>(BoxActor);
BoxActor->SetRootComponent(BoxRoot);
BoxRoot->RegisterComponent();
UBoxComponent* Box = NewObject<UBoxComponent>(BoxActor);
Box->SetupAttachment(BoxRoot);
Box->RegisterComponent();
Box->SetBoxExtent(FVector(100.0f, 100.0f, 100.0f));
// Point well inside the box, on an actor scaled to 0.1. The previous code multiplied the box
// extent by the queried actor's scale and would wrongly report this as outside.
TestTrue("Box: point inside is detected regardless of the queried actor's scale",
UDirectiveUtilEditorActorSubsystem::IsActorWithinBoxBounds(SpawnPoint(FVector(50.0f, 50.0f, 50.0f), FVector(0.1f)), Box));
TestFalse("Box: point beyond the extent is rejected",
UDirectiveUtilEditorActorSubsystem::IsActorWithinBoxBounds(SpawnPoint(FVector(250.0f, 0.0f, 0.0f), FVector::OneVector), Box));
// Capsule of radius 50 and half-height 100 at the origin.
AActor* CapsuleActor = World->SpawnActor<AActor>();
USceneComponent* CapsuleRoot = NewObject<USceneComponent>(CapsuleActor);
CapsuleActor->SetRootComponent(CapsuleRoot);
CapsuleRoot->RegisterComponent();
UCapsuleComponent* Capsule = NewObject<UCapsuleComponent>(CapsuleActor);
Capsule->SetupAttachment(CapsuleRoot);
Capsule->RegisterComponent();
Capsule->SetCapsuleSize(50.0f, 100.0f);
TestTrue("Capsule: point within the upper cap is detected",
UDirectiveUtilEditorActorSubsystem::IsActorWithinCapsuleBounds(SpawnPoint(FVector(0.0f, 0.0f, 90.0f), FVector::OneVector), Capsule));
TestTrue("Capsule: point within the radius is detected",
UDirectiveUtilEditorActorSubsystem::IsActorWithinCapsuleBounds(SpawnPoint(FVector(40.0f, 0.0f, 0.0f), FVector::OneVector), Capsule));
TestFalse("Capsule: point beyond the radius is rejected (not a broad sphere)",
UDirectiveUtilEditorActorSubsystem::IsActorWithinCapsuleBounds(SpawnPoint(FVector(60.0f, 0.0f, 0.0f), FVector::OneVector), Capsule));
TestFalse("Capsule: point beyond the end cap is rejected",
UDirectiveUtilEditorActorSubsystem::IsActorWithinCapsuleBounds(SpawnPoint(FVector(0.0f, 0.0f, 200.0f), FVector::OneVector), Capsule));
GEngine->DestroyWorldContext(World);
World->DestroyWorld(false);
return true;
}
#endif

View File

@@ -0,0 +1,51 @@
#if WITH_EDITOR
#include "Libraries/DirectiveUtilEditorAssetLibrary.h"
#include "Engine/World.h"
#include "Misc/AutomationTest.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorAssetLibraryTest, "DirectiveUtilities.EditorAssetLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilEditorAssetLibraryTest::RunTest(const FString& Parameters)
{
EDirectiveUtilSuccessStatus Status = EDirectiveUtilSuccessStatus::Success;
const FString DefaultName = UDirectiveUtilEditorAssetLibrary::GetDefaultAssetNameForClass(nullptr, Status);
TestEqual("GetDefaultAssetNameForClass should fail for a null class", Status, EDirectiveUtilSuccessStatus::Failure);
TestTrue("GetDefaultAssetNameForClass should return an empty name for a null class", DefaultName.IsEmpty());
Status = EDirectiveUtilSuccessStatus::Success;
const TArray<FAssetData> NoAssets = UDirectiveUtilEditorAssetLibrary::GetAssetsByClass(nullptr, TEXT("/Game"), false, true, Status);
TestEqual("GetAssetsByClass should fail for a null class", Status, EDirectiveUtilSuccessStatus::Failure);
TestEqual("GetAssetsByClass should return no assets for a null class", NoAssets.Num(), 0);
Status = EDirectiveUtilSuccessStatus::Success;
const TArray<FString> NoDependencies = UDirectiveUtilEditorAssetLibrary::GetAssetDependencies(FAssetData(), false, Status);
TestEqual("GetAssetDependencies should fail for an invalid asset", Status, EDirectiveUtilSuccessStatus::Failure);
TestEqual("GetAssetDependencies should return nothing for an invalid asset", NoDependencies.Num(), 0);
Status = EDirectiveUtilSuccessStatus::Success;
const TArray<FString> NoReferencers = UDirectiveUtilEditorAssetLibrary::GetAssetReferencers(FAssetData(), false, Status);
TestEqual("GetAssetReferencers should fail for an invalid asset", Status, EDirectiveUtilSuccessStatus::Failure);
TestEqual("GetAssetReferencers should return nothing for an invalid asset", NoReferencers.Num(), 0);
Status = EDirectiveUtilSuccessStatus::Failure;
UDirectiveUtilEditorAssetLibrary::GetAssetsByClass(UWorld::StaticClass(), TEXT("/Game"), false, true, Status);
TestEqual("GetAssetsByClass should succeed for a valid class query", Status, EDirectiveUtilSuccessStatus::Success);
const TMap<FDirectiveUtilAssetKey, FDirectiveUtilDuplicateAssetData> DuplicateAssets =
UDirectiveUtilEditorAssetLibrary::FindDuplicateAssets({TEXT("/Engine/BasicShapes"), TEXT("/Engine/BasicShapes")}, false);
for (const TPair<FDirectiveUtilAssetKey, FDirectiveUtilDuplicateAssetData>& Pair : DuplicateAssets)
{
TSet<FString> UniquePaths;
for (const FString& AssetPath : Pair.Value.DuplicateAssetPaths)
{
UniquePaths.Add(AssetPath);
}
TestEqual("FindDuplicateAssets should return each asset path once", UniquePaths.Num(), Pair.Value.DuplicateAssetPaths.Num());
}
return true;
}
#endif

View File

@@ -0,0 +1,97 @@
#include "Libraries/DirectiveUtilFunctionLibrary.h"
#include "Tests/AutomationCommon.h"
#include "Misc/AutomationTest.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilFunctionLibraryTest, "DirectiveUtilities.FunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilFunctionLibraryTest::RunTest(const FString& Parameters)
{
// Preserve the user/CI clipboard so this test doesn't destroy it, and restore it before returning.
const FString OriginalClipboard = UDirectiveUtilFunctionLibrary::GetStringFromClipboard();
const FText TestText = FText::FromString(TEXT("Hello, Clipboard!"));
UDirectiveUtilFunctionLibrary::CopyTextToClipboard(TestText);
const FText ClipboardText = UDirectiveUtilFunctionLibrary::GetTextFromClipboard();
TestEqual("GetTextFromClipboard should return the copied text", ClipboardText.ToString(), TestText.ToString());
ADD_LATENT_AUTOMATION_COMMAND(FEngineWaitLatentCommand(1.0f));
const FString TestString = TEXT("Hello, Clipboard!");
UDirectiveUtilFunctionLibrary::CopyStringToClipboard(TestString);
const FString ClipboardString = UDirectiveUtilFunctionLibrary::GetStringFromClipboard();
TestEqual("GetStringFromClipboard should return the copied string", ClipboardString, TestString);
ADD_LATENT_AUTOMATION_COMMAND(FEngineWaitLatentCommand(1.0f));
UDirectiveUtilFunctionLibrary::ClearClipboard();
const FString ClearedClipboardString = UDirectiveUtilFunctionLibrary::GetStringFromClipboard();
TestEqual("GetStringFromClipboard should return an empty string after clearing the clipboard", ClearedClipboardString, TEXT(""));
const FString ProjectVersion = UDirectiveUtilFunctionLibrary::GetProjectVersion();
TestNotEqual("GetProjectVersion should return a non-empty string", ProjectVersion, FString(""));
TestTrue("IsRunningInEditor should return true in editor context",
UDirectiveUtilFunctionLibrary::IsRunningInEditor());
// Pure pass-through to the engine's GetDerivedClasses (which appends to the output array).
// Happy path (recursive): the running module guarantees a stable hierarchy to query.
TArray<UClass*> RecursiveDerived;
UDirectiveUtilFunctionLibrary::GetChildClasses(UBlueprintFunctionLibrary::StaticClass(), true, RecursiveDerived);
TestTrue("GetChildClasses should return a non-empty list for a base class with subclasses",
RecursiveDerived.Num() > 0);
TestTrue("GetChildClasses should include a known derived class (UDirectiveUtilFunctionLibrary)",
RecursiveDerived.Contains(UDirectiveUtilFunctionLibrary::StaticClass()));
TestFalse("GetChildClasses should not include the base class itself",
RecursiveDerived.Contains(UBlueprintFunctionLibrary::StaticClass()));
// Non-recursive results must not exceed the recursive results for the same base.
TArray<UClass*> NonRecursiveDerived;
UDirectiveUtilFunctionLibrary::GetChildClasses(UBlueprintFunctionLibrary::StaticClass(), false, NonRecursiveDerived);
TestTrue("GetChildClasses non-recursive count should not exceed recursive count",
NonRecursiveDerived.Num() <= RecursiveDerived.Num());
// Leaf class with no subclasses: an empty input array should remain empty.
TArray<UClass*> LeafDerived;
UDirectiveUtilFunctionLibrary::GetChildClasses(UDirectiveUtilFunctionLibrary::StaticClass(), true, LeafDerived);
TestEqual("GetChildClasses should return an empty list for a class with no subclasses",
LeafDerived.Num(), 0);
// Null base class: should not crash and should add nothing.
TArray<UClass*> NullBaseDerived;
UDirectiveUtilFunctionLibrary::GetChildClasses(nullptr, true, NullBaseDerived);
TestEqual("GetChildClasses should return an empty list for a null base class",
NullBaseDerived.Num(), 0);
{
const TCHAR* CommandLine = TEXT("-Fast -Mode=Quality -Name=\"Big Save\"");
TestTrue("HasCommandLineSwitch should find a present switch",
UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(CommandLine, TEXT("Fast")));
TestTrue("HasCommandLineSwitch should match case-insensitively",
UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(CommandLine, TEXT("fast")));
TestFalse("HasCommandLineSwitch should not find an absent switch",
UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(CommandLine, TEXT("Slow")));
TestFalse("HasCommandLineSwitch should return false for an empty switch",
UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(CommandLine, TEXT("")));
FString Value;
TestTrue("GetCommandLineOption should find a present key",
UDirectiveUtilFunctionLibrary::GetCommandLineOption(CommandLine, TEXT("Mode"), Value));
TestEqual("GetCommandLineOption should return the key's value", Value, FString(TEXT("Quality")));
TestTrue("GetCommandLineOption should read a quoted value",
UDirectiveUtilFunctionLibrary::GetCommandLineOption(CommandLine, TEXT("Name"), Value));
TestEqual("GetCommandLineOption should return the quoted value without quotes", Value, FString(TEXT("Big Save")));
TestFalse("GetCommandLineOption should not find an absent key",
UDirectiveUtilFunctionLibrary::GetCommandLineOption(CommandLine, TEXT("Missing"), Value));
TestFalse("GetCommandLineOption should return false for an empty key",
UDirectiveUtilFunctionLibrary::GetCommandLineOption(CommandLine, TEXT(""), Value));
// Smoke test through the process-command-line path.
TestFalse("HasCommandLineSwitch should not find a switch that was never passed",
UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(TEXT("DirectiveUtilitiesDefinitelyNotPassed")));
}
UDirectiveUtilFunctionLibrary::CopyStringToClipboard(OriginalClipboard);
return true;
}

View File

@@ -0,0 +1,113 @@
#include "Libraries/DirectiveUtilGameplayTagFunctionLibrary.h"
#include "GameplayTagsManager.h"
#include "Misc/AutomationTest.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilGameplayTagFunctionLibraryTest, "DirectiveUtilities.GameplayTagFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilGameplayTagFunctionLibraryTest::RunTest(const FString& Parameters)
{
// Native tag registration is unavailable to a DeveloperTool module (module-type validation)
// and the legacy FName path ensures on 5.6/5.7 after startup. Register through a runtime
// ini tag source instead, which supports late addition from any module.
const FName TestTagName(TEXT("DirectiveUtilities.Test.Alpha.Beta"));
const FName SiblingTagName(TEXT("DirectiveUtilities.Test.Alpha.Gamma"));
const FName GrandchildTagName(TEXT("DirectiveUtilities.Test.Alpha.Beta.Delta"));
FGameplayTag Tag = FGameplayTag::RequestGameplayTag(TestTagName, false);
FGameplayTag SiblingTag = FGameplayTag::RequestGameplayTag(SiblingTagName, false);
FGameplayTag GrandchildTag = FGameplayTag::RequestGameplayTag(GrandchildTagName, false);
if (!Tag.IsValid() || !SiblingTag.IsValid() || !GrandchildTag.IsValid())
{
// AddTagIniSearchPath will not rescan an already-added directory, so growing the
// tag set requires a fresh directory name.
const FString TagIniDirectory = FPaths::ProjectSavedDir() / TEXT("DirectiveUtilitiesTests") / TEXT("Tags");
const FString TagIniPath = TagIniDirectory / TEXT("DirectiveUtilitiesTestTags.ini");
const FString TagIniContents = TEXT("[/Script/GameplayTags.GameplayTagsList]\n")
TEXT("GameplayTagList=(Tag=\"DirectiveUtilities.Test.Alpha.Beta\",DevComment=\"DirectiveUtilities automation test tag\")\n")
TEXT("GameplayTagList=(Tag=\"DirectiveUtilities.Test.Alpha.Gamma\",DevComment=\"DirectiveUtilities automation test tag\")\n")
TEXT("GameplayTagList=(Tag=\"DirectiveUtilities.Test.Alpha.Beta.Delta\",DevComment=\"DirectiveUtilities automation test tag\")\n");
if (!FFileHelper::SaveStringToFile(TagIniContents, *TagIniPath))
{
AddError(TEXT("Failed to write the test tag ini file."));
return false;
}
UGameplayTagsManager::Get().AddTagIniSearchPath(TagIniDirectory);
Tag = FGameplayTag::RequestGameplayTag(TestTagName, false);
SiblingTag = FGameplayTag::RequestGameplayTag(SiblingTagName, false);
GrandchildTag = FGameplayTag::RequestGameplayTag(GrandchildTagName, false);
}
TestTrue("The registered test tag should be valid", Tag.IsValid());
TestTrue("The registered sibling test tag should be valid", SiblingTag.IsValid());
TestTrue("The registered grandchild test tag should be valid", GrandchildTag.IsValid());
TestEqual("GetTagSegments should return each segment in order",
UDirectiveUtilGameplayTagFunctionLibrary::GetTagSegments(Tag),
TArray<FString>({TEXT("DirectiveUtilities"), TEXT("Test"), TEXT("Alpha"), TEXT("Beta")}));
TestEqual("GetTagDepth should return the segment count", UDirectiveUtilGameplayTagFunctionLibrary::GetTagDepth(Tag), 4);
TestEqual("GetTagLeafName should return the leaf segment", UDirectiveUtilGameplayTagFunctionLibrary::GetTagLeafName(Tag), FString(TEXT("Beta")));
const FGameplayTag DirectParent = UDirectiveUtilGameplayTagFunctionLibrary::GetTagDirectParent(Tag);
TestEqual("GetTagDirectParent should return the immediate parent", DirectParent.GetTagName(), FName(TEXT("DirectiveUtilities.Test.Alpha")));
const FGameplayTagContainer Parents = UDirectiveUtilGameplayTagFunctionLibrary::GetTagParents(Tag);
TestTrue("GetTagParents should contain the direct parent", Parents.HasTagExact(DirectParent));
TestFalse("GetTagParents should exclude the tag itself", Parents.HasTagExact(Tag));
const FGameplayTag InvalidTag;
TestFalse("GetTagDirectParent of an invalid tag should be invalid", UDirectiveUtilGameplayTagFunctionLibrary::GetTagDirectParent(InvalidTag).IsValid());
TestEqual("GetTagDepth of an invalid tag should be 0", UDirectiveUtilGameplayTagFunctionLibrary::GetTagDepth(InvalidTag), 0);
TestTrue("GetTagLeafName of an invalid tag should be empty", UDirectiveUtilGameplayTagFunctionLibrary::GetTagLeafName(InvalidTag).IsEmpty());
TestEqual("GetTagSegments of an invalid tag should be empty", UDirectiveUtilGameplayTagFunctionLibrary::GetTagSegments(InvalidTag).Num(), 0);
const FGameplayTag AlphaTag = FGameplayTag::RequestGameplayTag(FName(TEXT("DirectiveUtilities.Test.Alpha")), false);
TestTrue("The auto-registered parent tag should be valid", AlphaTag.IsValid());
const FGameplayTagContainer Children = UDirectiveUtilGameplayTagFunctionLibrary::GetTagChildren(AlphaTag);
TestTrue("GetTagChildren should contain the first direct child", Children.HasTagExact(Tag));
TestTrue("GetTagChildren should contain the second direct child", Children.HasTagExact(SiblingTag));
TestTrue("GetTagChildren should contain a grandchild", Children.HasTagExact(GrandchildTag));
const FGameplayTagContainer DirectChildren = UDirectiveUtilGameplayTagFunctionLibrary::GetTagDirectChildren(AlphaTag);
TestTrue("GetTagDirectChildren should contain the first direct child", DirectChildren.HasTagExact(Tag));
TestTrue("GetTagDirectChildren should contain the second direct child", DirectChildren.HasTagExact(SiblingTag));
TestFalse("GetTagDirectChildren should exclude grandchildren", DirectChildren.HasTagExact(GrandchildTag));
TestTrue("GetTagChildren of a leaf tag should be empty", UDirectiveUtilGameplayTagFunctionLibrary::GetTagChildren(GrandchildTag).IsEmpty());
TestEqual("GetTagCommonAncestor should return the deepest shared ancestor",
UDirectiveUtilGameplayTagFunctionLibrary::GetTagCommonAncestor(GrandchildTag, SiblingTag).GetTagName(), FName(TEXT("DirectiveUtilities.Test.Alpha")));
TestEqual("GetTagCommonAncestor may return one of the inputs",
UDirectiveUtilGameplayTagFunctionLibrary::GetTagCommonAncestor(Tag, GrandchildTag).GetTagName(), Tag.GetTagName());
TestTrue("GetTagChildren of an invalid tag should be empty", UDirectiveUtilGameplayTagFunctionLibrary::GetTagChildren(InvalidTag).IsEmpty());
TestTrue("GetTagDirectChildren of an invalid tag should be empty", UDirectiveUtilGameplayTagFunctionLibrary::GetTagDirectChildren(InvalidTag).IsEmpty());
TestFalse("GetTagCommonAncestor of an invalid tag should be invalid", UDirectiveUtilGameplayTagFunctionLibrary::GetTagCommonAncestor(InvalidTag, Tag).IsValid());
TestEqual("GetTagAtDepth should truncate to the requested depth",
UDirectiveUtilGameplayTagFunctionLibrary::GetTagAtDepth(GrandchildTag, 3).GetTagName(), FName(TEXT("DirectiveUtilities.Test.Alpha")));
TestEqual("GetTagAtDepth at the tag's own depth should return the tag",
UDirectiveUtilGameplayTagFunctionLibrary::GetTagAtDepth(GrandchildTag, 5).GetTagName(), GrandchildTag.GetTagName());
TestEqual("GetTagAtDepth beyond the tag's depth should return the tag",
UDirectiveUtilGameplayTagFunctionLibrary::GetTagAtDepth(GrandchildTag, 99).GetTagName(), GrandchildTag.GetTagName());
TestFalse("GetTagAtDepth of zero should be invalid", UDirectiveUtilGameplayTagFunctionLibrary::GetTagAtDepth(GrandchildTag, 0).IsValid());
TestFalse("GetTagAtDepth of an invalid tag should be invalid", UDirectiveUtilGameplayTagFunctionLibrary::GetTagAtDepth(InvalidTag, 2).IsValid());
const FGameplayTagContainer Siblings = UDirectiveUtilGameplayTagFunctionLibrary::GetTagSiblings(Tag);
TestTrue("GetTagSiblings should contain the sibling", Siblings.HasTagExact(SiblingTag));
TestFalse("GetTagSiblings should exclude the tag itself", Siblings.HasTagExact(Tag));
TestFalse("GetTagSiblings should exclude the tag's children", Siblings.HasTagExact(GrandchildTag));
TestTrue("GetTagSiblings of an invalid tag should be empty", UDirectiveUtilGameplayTagFunctionLibrary::GetTagSiblings(InvalidTag).IsEmpty());
TestTrue("IsLeafTag should be true for a childless tag", UDirectiveUtilGameplayTagFunctionLibrary::IsLeafTag(GrandchildTag));
TestFalse("IsLeafTag should be false for a tag with children", UDirectiveUtilGameplayTagFunctionLibrary::IsLeafTag(Tag));
TestFalse("IsLeafTag of an invalid tag should be false", UDirectiveUtilGameplayTagFunctionLibrary::IsLeafTag(InvalidTag));
const TArray<FGameplayTag> FoundTags = UDirectiveUtilGameplayTagFunctionLibrary::FindRegisteredTags(TEXT("test.alpha"));
TestTrue("FindRegisteredTags should match case-insensitively", FoundTags.Contains(Tag));
TestTrue("FindRegisteredTags should find every matching tag", FoundTags.Contains(SiblingTag) && FoundTags.Contains(GrandchildTag));
TestEqual("FindRegisteredTags with no match should be empty", UDirectiveUtilGameplayTagFunctionLibrary::FindRegisteredTags(TEXT("DirectiveUtilitiesNoSuchTagXYZ")).Num(), 0);
TestEqual("FindRegisteredTags with an empty substring should be empty", UDirectiveUtilGameplayTagFunctionLibrary::FindRegisteredTags(TEXT("")).Num(), 0);
return true;
}

View File

@@ -0,0 +1,146 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilInputFunctionLibrary.h"
#include "Types/DirectiveUtilInputTypes.h"
#include "Types/DirectiveUtilTypes.h"
#include "InputMappingContext.h"
#include "EnhancedInputSubsystems.h"
#include "Engine/GameInstance.h"
#include "Engine/LocalPlayer.h"
#include "Engine/World.h"
#include "Engine/Engine.h"
#include "GameFramework/PlayerController.h"
#include "Misc/AutomationTest.h"
#if WITH_EDITOR
/**
* Exercises the Enhanced Input library's active paths against a real EnhancedInput subsystem,
* which requires a live local player. A standalone game instance + local player is built for this;
* if that cannot be created in the headless harness the test skips its assertions rather than failing.
*/
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilInputActivePathTest, "DirectiveUtilities.InputActivePathTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilInputActivePathTest::RunTest(const FString& Parameters)
{
// These warnings only fire on the graceful-skip paths; allow (but never require) them.
AddExpectedMessagePlain(TEXT("LocalPlayer not found. Cannot set input mapping contexts."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
AddExpectedMessagePlain(TEXT("EnhancedInput subsystem not found."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
UGameInstance* GameInstance = NewObject<UGameInstance>(GEngine);
if (!GameInstance)
{
AddInfo(TEXT("Could not create a game instance; skipping Enhanced Input active-path assertions."));
return true;
}
GameInstance->AddToRoot();
GameInstance->InitializeStandalone();
UWorld* World = GameInstance->GetWorld();
// CreateLocalPlayer ensures when there is no game viewport client (headless), so build the local
// player directly via AddLocalPlayer, which initializes the local-player subsystems without one.
UClass* LocalPlayerClass = GEngine->LocalPlayerClass ? GEngine->LocalPlayerClass.Get() : ULocalPlayer::StaticClass();
ULocalPlayer* LocalPlayer = NewObject<ULocalPlayer>(GEngine, LocalPlayerClass);
if (World && LocalPlayer)
{
GameInstance->AddLocalPlayer(LocalPlayer, FPlatformMisc::GetPlatformUserForUserIndex(0));
}
if (!World || !LocalPlayer)
{
AddInfo(TEXT("Local player unavailable in the headless harness; skipping Enhanced Input active-path assertions."));
GameInstance->Shutdown();
GameInstance->RemoveFromRoot();
return true;
}
APlayerController* PlayerController = World->SpawnActor<APlayerController>();
if (!PlayerController)
{
AddInfo(TEXT("Could not spawn a player controller; skipping Enhanced Input active-path assertions."));
GameInstance->Shutdown();
GameInstance->RemoveFromRoot();
return true;
}
PlayerController->SetPlayer(LocalPlayer);
UEnhancedInputLocalPlayerSubsystem* Subsystem = UDirectiveUtilInputFunctionLibrary::GetEnhancedInputSubsystem(PlayerController);
if (!Subsystem)
{
AddInfo(TEXT("Enhanced Input subsystem unavailable for the synthetic local player; skipping active-path assertions."));
GameInstance->Shutdown();
GameInstance->RemoveFromRoot();
return true;
}
// We have a live subsystem: exercise the active add/active/swap/remove/clear paths for real.
TestNotNull("GetEnhancedInputSubsystem returns the subsystem for a live local player", Subsystem);
UInputMappingContext* ContextA = NewObject<UInputMappingContext>(GetTransientPackage());
UInputMappingContext* ContextB = NewObject<UInputMappingContext>(GetTransientPackage());
ContextA->AddToRoot();
ContextB->AddToRoot();
const TSoftObjectPtr<UInputMappingContext> SoftA(ContextA);
const TSoftObjectPtr<UInputMappingContext> SoftB(ContextB);
// Add context A.
FDirectiveUtilEnhancedInputContextData DataA;
DataA.InputContext = SoftA;
DataA.Priority = 0;
TArray<FDirectiveUtilEnhancedInputContextData> ToAdd;
ToAdd.Add(DataA);
TestEqual("AddInputMappingContexts succeeds for a live controller",
UDirectiveUtilInputFunctionLibrary::AddInputMappingContexts(PlayerController, ToAdd, false),
EDirectiveUtilSuccessStatus::Success);
TestTrue("Added context is reported active",
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftA));
// Swap A -> B.
TestEqual("SwapInputMappingContexts succeeds",
UDirectiveUtilInputFunctionLibrary::SwapInputMappingContexts(PlayerController, SoftA, SoftB, 0, false),
EDirectiveUtilSuccessStatus::Success);
TestFalse("Swapped-out context is inactive",
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftA));
TestTrue("Swapped-in context is active",
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftB));
// Remove B.
TArray<TSoftObjectPtr<UInputMappingContext>> ToRemove;
ToRemove.Add(SoftB);
TestEqual("RemoveInputMappingContexts succeeds",
UDirectiveUtilInputFunctionLibrary::RemoveInputMappingContexts(PlayerController, ToRemove),
EDirectiveUtilSuccessStatus::Success);
TestFalse("Removed context is inactive",
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftB));
// Clear all (after re-adding A).
UDirectiveUtilInputFunctionLibrary::AddInputMappingContexts(PlayerController, ToAdd, false);
TestEqual("ClearAllInputMappingContexts succeeds",
UDirectiveUtilInputFunctionLibrary::ClearAllInputMappingContexts(PlayerController),
EDirectiveUtilSuccessStatus::Success);
TestFalse("Context is inactive after clear-all",
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftA));
// A clearing add whose every context fails to load must leave the existing mappings untouched.
AddExpectedMessagePlain(TEXT("Input Mapping Contexts failed to load and were not added!"), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
UDirectiveUtilInputFunctionLibrary::AddInputMappingContexts(PlayerController, ToAdd, false);
FDirectiveUtilEnhancedInputContextData UnresolvableData;
UnresolvableData.Priority = 0;
TArray<FDirectiveUtilEnhancedInputContextData> UnresolvableToAdd;
UnresolvableToAdd.Add(UnresolvableData);
TestEqual("AddInputMappingContexts fails when no context loads",
UDirectiveUtilInputFunctionLibrary::AddInputMappingContexts(PlayerController, UnresolvableToAdd, true),
EDirectiveUtilSuccessStatus::Failure);
TestTrue("Previously active context survives a failed clearing add",
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftA));
ContextA->RemoveFromRoot();
ContextB->RemoveFromRoot();
GameInstance->Shutdown();
GameInstance->RemoveFromRoot();
return true;
}
#endif // WITH_EDITOR

View File

@@ -0,0 +1,62 @@
#include "Libraries/DirectiveUtilInputFunctionLibrary.h"
#include "Types/DirectiveUtilTypes.h"
#include "InputMappingContext.h"
#include "Misc/AutomationTest.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilInputFunctionLibraryTest, "DirectiveUtilities.InputFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilInputFunctionLibraryTest::RunTest(const FString& Parameters)
{
// The null-controller and invalid-context paths intentionally log warnings. Register them as
// expected (plain match, negative count = consume if present, never required) so the run is clean.
AddExpectedMessagePlain(TEXT("PlayerController is null. Cannot set input mapping contexts."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
AddExpectedMessagePlain(TEXT("Both the previous and new input mapping contexts must be valid."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
// AddInputMappingContexts should return Failure with null controller
TArray<FDirectiveUtilEnhancedInputContextData> Contexts;
FDirectiveUtilEnhancedInputContextData Context;
Contexts.Add(Context);
TestEqual("AddInputMappingContexts should fail with null controller",
UDirectiveUtilInputFunctionLibrary::AddInputMappingContexts(nullptr, Contexts, false),
EDirectiveUtilSuccessStatus::Failure);
// RemoveInputMappingContexts should return Failure with null controller
TArray<TSoftObjectPtr<UInputMappingContext>> RemoveContexts;
RemoveContexts.Add(nullptr);
TestEqual("RemoveInputMappingContexts should fail with null controller",
UDirectiveUtilInputFunctionLibrary::RemoveInputMappingContexts(nullptr, RemoveContexts),
EDirectiveUtilSuccessStatus::Failure);
// AddInputMappingContexts should return Failure with empty contexts
TArray<FDirectiveUtilEnhancedInputContextData> EmptyContexts;
TestEqual("AddInputMappingContexts should fail with empty contexts",
UDirectiveUtilInputFunctionLibrary::AddInputMappingContexts(nullptr, EmptyContexts, false),
EDirectiveUtilSuccessStatus::Failure);
// RemoveInputMappingContexts should fail with empty contexts (early-return before touching the controller)
TArray<TSoftObjectPtr<UInputMappingContext>> EmptyRemoveContexts;
TestEqual("RemoveInputMappingContexts should fail with empty contexts",
UDirectiveUtilInputFunctionLibrary::RemoveInputMappingContexts(nullptr, EmptyRemoveContexts),
EDirectiveUtilSuccessStatus::Failure);
// SwapInputMappingContexts loads both contexts before using the controller, so invalid
// (unset) soft pointers must return Failure regardless of the (null) controller.
const TSoftObjectPtr<UInputMappingContext> NullContext;
TestEqual("SwapInputMappingContexts should fail when both contexts are invalid",
UDirectiveUtilInputFunctionLibrary::SwapInputMappingContexts(nullptr, NullContext, NullContext, 0, false),
EDirectiveUtilSuccessStatus::Failure);
TestEqual("SwapInputMappingContexts should fail with invalid contexts even when using previous priority",
UDirectiveUtilInputFunctionLibrary::SwapInputMappingContexts(nullptr, NullContext, NullContext, 5, true),
EDirectiveUtilSuccessStatus::Failure);
// New subsystem-getter helpers: null-controller safety (the active paths require a live player controller).
TestNull("GetEnhancedInputSubsystem should return null for a null controller",
UDirectiveUtilInputFunctionLibrary::GetEnhancedInputSubsystem(nullptr));
TestFalse("IsInputMappingContextActive should return false for a null controller",
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(nullptr, NullContext));
TestEqual("ClearAllInputMappingContexts should fail for a null controller",
UDirectiveUtilInputFunctionLibrary::ClearAllInputMappingContexts(nullptr),
EDirectiveUtilSuccessStatus::Failure);
return true;
}

View File

@@ -0,0 +1,148 @@
#include "Libraries/DirectiveUtilMapFunctionLibrary.h"
#include "Tests/DirectiveUtilTestObject.h"
#include "Misc/AutomationTest.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMapFunctionLibraryTest, "DirectiveUtilities.MapFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilMapFunctionLibraryTest::RunTest(const FString& Parameters)
{
UDirectiveUtilTestObject* TestObject = NewObject<UDirectiveUtilTestObject>();
FMapProperty* MapProperty = FindFProperty<FMapProperty>(UDirectiveUtilTestObject::StaticClass(), GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestMap));
TestNotNull("TestMap property should be found", MapProperty);
if (!MapProperty)
{
return false;
}
TestObject->TestMap = {{1, 100}, {2, 200}};
int32 ExistingKey = 2;
int32 FoundValue = -1;
UDirectiveUtilMapFunctionLibrary::GenericMap_FindOrAdd(&TestObject->TestMap, MapProperty, &ExistingKey, &FoundValue);
TestEqual("FindOrAdd should return the existing value for a present key", FoundValue, 200);
TestEqual("FindOrAdd should not change the map size for a present key", TestObject->TestMap.Num(), 2);
int32 MissingKey = 3;
int32 AddedValue = -1;
UDirectiveUtilMapFunctionLibrary::GenericMap_FindOrAdd(&TestObject->TestMap, MapProperty, &MissingKey, &AddedValue);
TestEqual("FindOrAdd should return the default value for a missing key", AddedValue, 0);
TestEqual("FindOrAdd should grow the map for a missing key", TestObject->TestMap.Num(), 3);
TestTrue("FindOrAdd should insert the missing key", TestObject->TestMap.Contains(3));
if (const int32* AddedEntry = TestObject->TestMap.Find(3))
{
TestEqual("FindOrAdd should store a default value for the new key", *AddedEntry, 0);
}
TestObject->TestMap = {{1, 100}, {2, 200}, {3, 300}};
UDirectiveUtilMapFunctionLibrary::GenericMap_ClearValues(&TestObject->TestMap, MapProperty);
TestEqual("ClearValues should preserve the number of entries", TestObject->TestMap.Num(), 3);
TestTrue("ClearValues should preserve key 1", TestObject->TestMap.Contains(1));
TestTrue("ClearValues should preserve key 2", TestObject->TestMap.Contains(2));
TestTrue("ClearValues should preserve key 3", TestObject->TestMap.Contains(3));
for (const TPair<int32, int32>& Pair : TestObject->TestMap)
{
TestEqual("ClearValues should reset every value to its default", Pair.Value, 0);
}
TestObject->TestMap.Empty();
UDirectiveUtilMapFunctionLibrary::GenericMap_ClearValues(&TestObject->TestMap, MapProperty);
TestEqual("ClearValues on an empty map should leave it empty", TestObject->TestMap.Num(), 0);
FArrayProperty* ArrayProperty = FindFProperty<FArrayProperty>(UDirectiveUtilTestObject::StaticClass(), GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestArray));
FMapProperty* MapProperty2 = FindFProperty<FMapProperty>(UDirectiveUtilTestObject::StaticClass(), GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestMap2));
FMapProperty* StringKeyMapProperty = FindFProperty<FMapProperty>(UDirectiveUtilTestObject::StaticClass(), GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestStringKeyMap));
TestNotNull("TestArray property should be found", ArrayProperty);
TestNotNull("TestMap2 property should be found", MapProperty2);
TestNotNull("TestStringKeyMap property should be found", StringKeyMapProperty);
if (!ArrayProperty || !MapProperty2 || !StringKeyMapProperty)
{
return false;
}
TestObject->TestMap = {{1, 10}, {2, 20}, {3, 10}};
TestObject->TestArray = {42};
int32 SearchValue = 10;
UDirectiveUtilMapFunctionLibrary::GenericMap_GetKeysByValue(&TestObject->TestMap, MapProperty, &SearchValue, &TestObject->TestArray, ArrayProperty);
TestEqual("GetKeysByValue should find two keys for a shared value", TestObject->TestArray.Num(), 2);
if (TestObject->TestArray.Num() == 2)
{
TestEqual("GetKeysByValue should return the first matching key", TestObject->TestArray[0], 1);
TestEqual("GetKeysByValue should return the second matching key", TestObject->TestArray[1], 3);
}
SearchValue = 99;
UDirectiveUtilMapFunctionLibrary::GenericMap_GetKeysByValue(&TestObject->TestMap, MapProperty, &SearchValue, &TestObject->TestArray, ArrayProperty);
TestEqual("GetKeysByValue should return no keys for an absent value", TestObject->TestArray.Num(), 0);
TestObject->TestArray = {42};
SearchValue = 10;
UDirectiveUtilMapFunctionLibrary::GenericMap_GetKeysByValue(nullptr, MapProperty, &SearchValue, &TestObject->TestArray, ArrayProperty);
TestEqual("GetKeysByValue with a null map should empty the output array", TestObject->TestArray.Num(), 0);
TestObject->TestArray = {42};
UDirectiveUtilMapFunctionLibrary::GenericMap_GetKeysByValue(&TestObject->TestMap, nullptr, &SearchValue, &TestObject->TestArray, ArrayProperty);
TestEqual("GetKeysByValue with a null map property should empty the output array", TestObject->TestArray.Num(), 0);
TestObject->TestStringKeyMap = {{TEXT("One"), 10}};
TestObject->TestArray = {42};
UDirectiveUtilMapFunctionLibrary::GenericMap_GetKeysByValue(&TestObject->TestStringKeyMap, StringKeyMapProperty, &SearchValue, &TestObject->TestArray, ArrayProperty);
TestEqual("GetKeysByValue with a mismatched key type should empty the output array", TestObject->TestArray.Num(), 0);
TestObject->TestMap = {{1, 10}, {2, 20}, {3, 10}};
int32 QueryValue = 20;
TestTrue("HasValue should find a present value", UDirectiveUtilMapFunctionLibrary::GenericMap_HasValue(&TestObject->TestMap, MapProperty, &QueryValue));
QueryValue = 99;
TestFalse("HasValue should not find an absent value", UDirectiveUtilMapFunctionLibrary::GenericMap_HasValue(&TestObject->TestMap, MapProperty, &QueryValue));
TestObject->TestMap.Empty();
QueryValue = 20;
TestFalse("HasValue on an empty map should be false", UDirectiveUtilMapFunctionLibrary::GenericMap_HasValue(&TestObject->TestMap, MapProperty, &QueryValue));
TestFalse("HasValue with a null map should be false", UDirectiveUtilMapFunctionLibrary::GenericMap_HasValue(nullptr, MapProperty, &QueryValue));
TestFalse("HasValue with a null map property should be false", UDirectiveUtilMapFunctionLibrary::GenericMap_HasValue(&TestObject->TestMap, nullptr, &QueryValue));
TestObject->TestMap = {{1, 10}, {2, 20}, {3, 10}};
TestObject->TestArray = {1, 3, 99};
int32 NumRemoved = UDirectiveUtilMapFunctionLibrary::GenericMap_RemoveKeys(&TestObject->TestMap, MapProperty, &TestObject->TestArray, ArrayProperty);
TestEqual("RemoveKeys should report two removed entries", NumRemoved, 2);
TestEqual("RemoveKeys should leave one entry behind", TestObject->TestMap.Num(), 1);
TestTrue("RemoveKeys should preserve the untouched key", TestObject->TestMap.Contains(2));
TestEqual("RemoveKeys with a null map should remove nothing", UDirectiveUtilMapFunctionLibrary::GenericMap_RemoveKeys(nullptr, MapProperty, &TestObject->TestArray, ArrayProperty), 0);
TestEqual("RemoveKeys with a null map property should remove nothing", UDirectiveUtilMapFunctionLibrary::GenericMap_RemoveKeys(&TestObject->TestMap, nullptr, &TestObject->TestArray, ArrayProperty), 0);
TestEqual("RemoveKeys with a mismatched key type should remove nothing", UDirectiveUtilMapFunctionLibrary::GenericMap_RemoveKeys(&TestObject->TestStringKeyMap, StringKeyMapProperty, &TestObject->TestArray, ArrayProperty), 0);
TestEqual("RemoveKeys with a mismatched key type should not change the map", TestObject->TestStringKeyMap.Num(), 1);
TestObject->TestMap = {{1, 10}};
TestObject->TestMap2 = {{1, 99}, {4, 40}};
UDirectiveUtilMapFunctionLibrary::GenericMap_Append(&TestObject->TestMap, MapProperty, &TestObject->TestMap2, MapProperty2, true);
TestEqual("Append with overwrite should end with two entries", TestObject->TestMap.Num(), 2);
if (const int32* OverwrittenValue = TestObject->TestMap.Find(1))
{
TestEqual("Append with overwrite should replace the existing value", *OverwrittenValue, 99);
}
if (const int32* AppendedValue = TestObject->TestMap.Find(4))
{
TestEqual("Append with overwrite should copy the new pair", *AppendedValue, 40);
}
TestObject->TestMap = {{1, 10}};
UDirectiveUtilMapFunctionLibrary::GenericMap_Append(&TestObject->TestMap, MapProperty, &TestObject->TestMap2, MapProperty2, false);
TestEqual("Append without overwrite should end with two entries", TestObject->TestMap.Num(), 2);
if (const int32* PreservedValue = TestObject->TestMap.Find(1))
{
TestEqual("Append without overwrite should preserve the existing value", *PreservedValue, 10);
}
if (const int32* NewValue = TestObject->TestMap.Find(4))
{
TestEqual("Append without overwrite should still copy the new pair", *NewValue, 40);
}
TestObject->TestMap = {{1, 10}};
UDirectiveUtilMapFunctionLibrary::GenericMap_Append(nullptr, MapProperty, &TestObject->TestMap2, MapProperty2, true);
UDirectiveUtilMapFunctionLibrary::GenericMap_Append(&TestObject->TestMap, nullptr, &TestObject->TestMap2, MapProperty2, true);
UDirectiveUtilMapFunctionLibrary::GenericMap_Append(&TestObject->TestMap, MapProperty, &TestObject->TestStringKeyMap, StringKeyMapProperty, true);
TestEqual("Append with null or mismatched properties should not change the target", TestObject->TestMap.Num(), 1);
if (const int32* UntouchedValue = TestObject->TestMap.Find(1))
{
TestEqual("Append with null or mismatched properties should preserve the value", *UntouchedValue, 10);
}
return true;
}

View File

@@ -0,0 +1,245 @@
#include "Libraries/DirectiveUtilMathFunctionLibrary.h"
#include "Misc/AutomationTest.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMathFunctionLibraryTest, "DirectiveUtilities.MathFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilMathFunctionLibraryTest::RunTest(const FString& Parameters)
{
TestEqual("AngleBetweenVectors should return 0 for parallel vectors",
UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ForwardVector, FVector::ForwardVector), 0.0f);
TestTrue("AngleBetweenVectors should return ~90 for perpendicular vectors",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ForwardVector, FVector::RightVector), 90.0f, 0.01f));
TestTrue("AngleBetweenVectors should return ~180 for opposite vectors",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ForwardVector, -FVector::ForwardVector), 180.0f, 0.01f));
TestTrue("AngleBetweenVectors should handle zero vector gracefully",
FMath::IsFinite(UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ZeroVector, FVector::ForwardVector)));
{
const FVector2D Sample2D(12.34f, 56.78f);
const float Noise2DFirst = UDirectiveUtilMathFunctionLibrary::PerlinNoise2D(Sample2D);
const float Noise2DSecond = UDirectiveUtilMathFunctionLibrary::PerlinNoise2D(Sample2D);
TestEqual("PerlinNoise2D should be deterministic for the same input", Noise2DFirst, Noise2DSecond);
TestTrue("PerlinNoise2D should return a finite value", FMath::IsFinite(Noise2DFirst));
TestTrue("PerlinNoise2D should be within [-1, 1]",
Noise2DFirst >= -1.0f - 1.e-4f && Noise2DFirst <= 1.0f + 1.e-4f);
}
{
const FVector2D Samples2D[] = {
FVector2D(0.5f, 0.5f),
FVector2D(-3.25f, 7.1f),
FVector2D(100.123f, -200.456f),
FVector2D(0.0f, 0.0f)
};
for (const FVector2D& Sample : Samples2D)
{
const float Value = UDirectiveUtilMathFunctionLibrary::PerlinNoise2D(Sample);
TestTrue(FString::Printf(TEXT("PerlinNoise2D should be finite at %s"), *Sample.ToString()), FMath::IsFinite(Value));
TestTrue(FString::Printf(TEXT("PerlinNoise2D should be within [-1,1] at %s"), *Sample.ToString()),
Value >= -1.0f - 1.e-4f && Value <= 1.0f + 1.e-4f);
}
}
TestTrue("PerlinNoise2D should be ~0 at an integer lattice point",
FMath::IsNearlyZero(UDirectiveUtilMathFunctionLibrary::PerlinNoise2D(FVector2D(3.0f, 4.0f)), 1.e-4f));
{
const FVector Sample3D(12.34f, 56.78f, 90.12f);
const float Noise3DFirst = UDirectiveUtilMathFunctionLibrary::PerlinNoise3D(Sample3D);
const float Noise3DSecond = UDirectiveUtilMathFunctionLibrary::PerlinNoise3D(Sample3D);
TestEqual("PerlinNoise3D should be deterministic for the same input", Noise3DFirst, Noise3DSecond);
TestTrue("PerlinNoise3D should return a finite value", FMath::IsFinite(Noise3DFirst));
TestTrue("PerlinNoise3D should be within [-1, 1]",
Noise3DFirst >= -1.0f - 1.e-4f && Noise3DFirst <= 1.0f + 1.e-4f);
}
{
const FVector Samples3D[] = {
FVector(0.5f, 0.5f, 0.5f),
FVector(-3.25f, 7.1f, -1.9f),
FVector(100.123f, -200.456f, 33.7f),
FVector(0.0f, 0.0f, 0.0f)
};
for (const FVector& Sample : Samples3D)
{
const float Value = UDirectiveUtilMathFunctionLibrary::PerlinNoise3D(Sample);
TestTrue(FString::Printf(TEXT("PerlinNoise3D should be finite at %s"), *Sample.ToString()), FMath::IsFinite(Value));
TestTrue(FString::Printf(TEXT("PerlinNoise3D should be within [-1,1] at %s"), *Sample.ToString()),
Value >= -1.0f - 1.e-4f && Value <= 1.0f + 1.e-4f);
}
}
TestTrue("PerlinNoise3D should be ~0 at an integer lattice point",
FMath::IsNearlyZero(UDirectiveUtilMathFunctionLibrary::PerlinNoise3D(FVector(3.0f, 4.0f, 5.0f)), 1.e-4f));
const TArray<EDirectiveUtilEaseType> AllEaseTypes = {
EDirectiveUtilEaseType::BackIn, EDirectiveUtilEaseType::BackOut, EDirectiveUtilEaseType::BackInOut,
EDirectiveUtilEaseType::ElasticIn, EDirectiveUtilEaseType::ElasticOut, EDirectiveUtilEaseType::ElasticInOut,
EDirectiveUtilEaseType::BounceIn, EDirectiveUtilEaseType::BounceOut, EDirectiveUtilEaseType::BounceInOut
};
for (const EDirectiveUtilEaseType EaseType : AllEaseTypes)
{
const FString TypeName = FString::FromInt(static_cast<int32>(EaseType));
TestTrue(FString::Printf(TEXT("EaseAlpha(0) should be ~0 for type %s"), *TypeName),
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::EaseAlpha(0.0f, EaseType), 0.0f, 1.e-3f));
TestTrue(FString::Printf(TEXT("EaseAlpha(1) should be ~1 for type %s"), *TypeName),
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::EaseAlpha(1.0f, EaseType), 1.0f, 1.e-3f));
for (const float Sample : {0.0f, 0.25f, 0.5f, 0.75f, 1.0f})
{
TestTrue(FString::Printf(TEXT("EaseAlpha(%.2f) should be finite for type %s"), Sample, *TypeName),
FMath::IsFinite(UDirectiveUtilMathFunctionLibrary::EaseAlpha(Sample, EaseType)));
}
}
TestEqual("EaseAlpha should clamp alpha below 0",
UDirectiveUtilMathFunctionLibrary::EaseAlpha(-1.0f, EDirectiveUtilEaseType::BounceOut),
UDirectiveUtilMathFunctionLibrary::EaseAlpha(0.0f, EDirectiveUtilEaseType::BounceOut));
TestEqual("EaseAlpha should clamp alpha above 1",
UDirectiveUtilMathFunctionLibrary::EaseAlpha(2.0f, EDirectiveUtilEaseType::BounceOut),
UDirectiveUtilMathFunctionLibrary::EaseAlpha(1.0f, EDirectiveUtilEaseType::BounceOut));
for (const EDirectiveUtilEaseType BounceType : {EDirectiveUtilEaseType::BounceIn, EDirectiveUtilEaseType::BounceOut, EDirectiveUtilEaseType::BounceInOut})
{
for (const float Sample : {0.1f, 0.3f, 0.6f, 0.9f})
{
const float Eased = UDirectiveUtilMathFunctionLibrary::EaseAlpha(Sample, BounceType);
TestTrue("Bounce easing should stay within [0,1]", Eased >= -1.e-3f && Eased <= 1.0f + 1.e-3f);
}
}
TestTrue("EaseFloat at alpha 0 returns A",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::EaseFloat(10.0f, 20.0f, 0.0f, EDirectiveUtilEaseType::BounceOut), 10.0f, 1.e-3f));
TestTrue("EaseFloat at alpha 1 returns B",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::EaseFloat(10.0f, 20.0f, 1.0f, EDirectiveUtilEaseType::BounceOut), 20.0f, 1.e-3f));
TestTrue("EaseVector at alpha 1 returns B",
UDirectiveUtilMathFunctionLibrary::EaseVector(FVector::ZeroVector, FVector(1, 2, 3), 1.0f, EDirectiveUtilEaseType::BounceOut).Equals(FVector(1, 2, 3), 1.e-2f));
TestTrue("EaseRotator at alpha 0 returns A",
UDirectiveUtilMathFunctionLibrary::EaseRotator(FRotator(10, 20, 30), FRotator(40, 50, 60), 0.0f, EDirectiveUtilEaseType::BounceOut).Equals(FRotator(10, 20, 30), 1.e-1f));
TestTrue("EaseColor at alpha 1 returns B",
UDirectiveUtilMathFunctionLibrary::EaseColor(FLinearColor::Black, FLinearColor::White, 1.0f, EDirectiveUtilEaseType::BounceOut).Equals(FLinearColor::White, 1.e-2f));
TestTrue("RoundToDecimals(3.14159, 2) ~= 3.14",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(3.14159f, 2), 3.14f, 1.e-4f));
TestTrue("RoundToDecimals(2.71828, 2) ~= 2.72 (rounds up)",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(2.71828f, 2), 2.72f, 1.e-4f));
TestTrue("RoundToDecimals(-1.2367, 2) ~= -1.24",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(-1.2367f, 2), -1.24f, 1.e-4f));
TestTrue("RoundToDecimals with 0 decimals rounds to integer",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(1.6f, 0), 2.0f, 1.e-4f));
TestTrue("RoundToDecimals(10.0, 5) ~= 10.0",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(10.0f, 5), 10.0f, 1.e-4f));
TestTrue("RoundToDecimals clamps excessive decimals without crashing",
FMath::IsFinite(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(1.23456789f, 50)));
TestTrue("RoundToDecimalsAsText(3.14159, 2) reads ~3.14",
FMath::IsNearlyEqual(FCString::Atof(*UDirectiveUtilMathFunctionLibrary::RoundToDecimalsAsText(3.14159f, 2).ToString()), 3.14f, 1.e-2f));
TestTrue("RoundToDecimals(2.5, 0) rounds half away from zero to 3",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(2.5f, 0), 3.0f, 1.e-4f));
TestTrue("RoundToDecimals(-2.5, 0) rounds half away from zero to -3",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(-2.5f, 0), -3.0f, 1.e-4f));
TestTrue("RoundToDecimals(0.125, 2) rounds half away from zero to 0.13",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RoundToDecimals(0.125f, 2), 0.13f, 0.0001f));
TestEqual("RoundToDecimalsAsText(2.5, 0) rounds half away from zero to \"3\"",
UDirectiveUtilMathFunctionLibrary::RoundToDecimalsAsText(2.5f, 0).ToString(), FString(TEXT("3")));
TestEqual("RoundToDecimalsAsText(-2.5, 0) rounds half away from zero to \"-3\"",
UDirectiveUtilMathFunctionLibrary::RoundToDecimalsAsText(-2.5f, 0).ToString(), FString(TEXT("-3")));
TestEqual("Weighted random on an empty array returns INDEX_NONE",
UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights(TArray<float>()), static_cast<int32>(INDEX_NONE));
TestEqual("Weighted random with all-zero weights returns INDEX_NONE",
UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights({0.0f, 0.0f, 0.0f}), static_cast<int32>(INDEX_NONE));
for (int32 Iteration = 0; Iteration < 25; ++Iteration)
{
TestEqual("Weighted random {0,1,0} always selects index 1",
UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights({0.0f, 1.0f, 0.0f}), 1);
TestEqual("Weighted random {5,0,0} always selects index 0",
UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights({5.0f, 0.0f, 0.0f}), 0);
}
TestEqual("FormatBytes(532) is \"532 B\"",
UDirectiveUtilMathFunctionLibrary::FormatBytes(532).ToString(), FString(TEXT("532 B")));
TestEqual("FormatBytes(1536) is \"1.5 KB\"",
UDirectiveUtilMathFunctionLibrary::FormatBytes(1536).ToString(), FString(TEXT("1.5 KB")));
TestEqual("FormatBytes(1450000, 1) is \"1.4 MB\"",
UDirectiveUtilMathFunctionLibrary::FormatBytes(1450000, 1).ToString(), FString(TEXT("1.4 MB")));
TestEqual("FormatBytes(0) is \"0 B\"",
UDirectiveUtilMathFunctionLibrary::FormatBytes(0).ToString(), FString(TEXT("0 B")));
TestEqual("FormatDuration(3785) is \"1h 03m 05s\"",
UDirectiveUtilMathFunctionLibrary::FormatDuration(3785.0f).ToString(), FString(TEXT("1h 03m 05s")));
TestEqual("FormatDuration(3785, false) is \"1h 03m\"",
UDirectiveUtilMathFunctionLibrary::FormatDuration(3785.0f, false).ToString(), FString(TEXT("1h 03m")));
TestEqual("FormatDuration(45) is \"45s\"",
UDirectiveUtilMathFunctionLibrary::FormatDuration(45.0f).ToString(), FString(TEXT("45s")));
TestEqual("FormatDuration(-90) is \"-1m 30s\"",
UDirectiveUtilMathFunctionLibrary::FormatDuration(-90.0f).ToString(), FString(TEXT("-1m 30s")));
TestEqual("FormatRelativeTime 5 minutes back reads \"5 minutes ago\"",
UDirectiveUtilMathFunctionLibrary::FormatRelativeTime(FDateTime::Now() - FTimespan::FromMinutes(5)).ToString(), FString(TEXT("5 minutes ago")));
TestEqual("FormatRelativeTime 10 seconds back reads \"just now\"",
UDirectiveUtilMathFunctionLibrary::FormatRelativeTime(FDateTime::Now() - FTimespan::FromSeconds(10)).ToString(), FString(TEXT("just now")));
TestEqual("FormatRelativeTime 2 hours ahead reads \"in 2 hours\"",
UDirectiveUtilMathFunctionLibrary::FormatRelativeTime(FDateTime::Now() + FTimespan::FromHours(2)).ToString(), FString(TEXT("in 2 hours")));
TestEqual("FormatRelativeTime 1 minute back reads \"1 minute ago\"",
UDirectiveUtilMathFunctionLibrary::FormatRelativeTime(FDateTime::Now() - FTimespan::FromMinutes(1)).ToString(), FString(TEXT("1 minute ago")));
{
const TArray<int32> IntValues = {1, 2, 3, 4};
TestEqual("GetIntArraySum({1,2,3,4}) is 10",
UDirectiveUtilMathFunctionLibrary::GetIntArraySum(IntValues), static_cast<int64>(10));
TestTrue("GetIntArrayAverage({1,2,3,4}) is 2.5",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetIntArrayAverage(IntValues), 2.5f, 1.e-4f));
TestTrue("GetIntArrayMedian({1,2,3,4}) averages the two middle values to 2.5",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian(IntValues), 2.5f, 1.e-4f));
TestTrue("GetIntArrayMedian({1,2,3}) is 2",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian({1, 2, 3}), 2.0f, 1.e-4f));
TestTrue("GetIntArrayMedian handles an unsorted input",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian({3, 1, 2}), 2.0f, 1.e-4f));
TestTrue("GetIntArrayStandardDeviation({2,4,4,4,5,5,7,9}) is the population value 2",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetIntArrayStandardDeviation({2, 4, 4, 4, 5, 5, 7, 9}), 2.0f, 1.e-4f));
TestEqual("GetIntArraySum({MAX_int32, MAX_int32}) does not overflow",
UDirectiveUtilMathFunctionLibrary::GetIntArraySum({MAX_int32, MAX_int32}), static_cast<int64>(MAX_int32) * 2);
const TArray<float> FloatValues = {1.0f, 2.0f, 3.0f, 4.0f};
TestTrue("GetFloatArraySum({1,2,3,4}) is 10",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetFloatArraySum(FloatValues), 10.0f, 1.e-4f));
TestTrue("GetFloatArrayAverage({1,2,3,4}) is 2.5",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetFloatArrayAverage(FloatValues), 2.5f, 1.e-4f));
TestTrue("GetFloatArrayMedian({1,2,3,4}) averages the two middle values to 2.5",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetFloatArrayMedian(FloatValues), 2.5f, 1.e-4f));
TestTrue("GetFloatArrayMedian({1,2,3}) is 2",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetFloatArrayMedian({1.0f, 2.0f, 3.0f}), 2.0f, 1.e-4f));
TestTrue("GetFloatArrayStandardDeviation({2,4,4,4,5,5,7,9}) is the population value 2",
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetFloatArrayStandardDeviation({2.0f, 4.0f, 4.0f, 4.0f, 5.0f, 5.0f, 7.0f, 9.0f}), 2.0f, 1.e-4f));
const TArray<int32> EmptyInts;
const TArray<float> EmptyFloats;
TestEqual("GetIntArraySum of an empty array is 0",
UDirectiveUtilMathFunctionLibrary::GetIntArraySum(EmptyInts), static_cast<int64>(0));
TestEqual("GetIntArrayAverage of an empty array is 0",
UDirectiveUtilMathFunctionLibrary::GetIntArrayAverage(EmptyInts), 0.0f);
TestEqual("GetIntArrayMedian of an empty array is 0",
UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian(EmptyInts), 0.0f);
TestEqual("GetIntArrayStandardDeviation of an empty array is 0",
UDirectiveUtilMathFunctionLibrary::GetIntArrayStandardDeviation(EmptyInts), 0.0f);
TestEqual("GetFloatArraySum of an empty array is 0",
UDirectiveUtilMathFunctionLibrary::GetFloatArraySum(EmptyFloats), 0.0f);
TestEqual("GetFloatArrayAverage of an empty array is 0",
UDirectiveUtilMathFunctionLibrary::GetFloatArrayAverage(EmptyFloats), 0.0f);
TestEqual("GetFloatArrayMedian of an empty array is 0",
UDirectiveUtilMathFunctionLibrary::GetFloatArrayMedian(EmptyFloats), 0.0f);
TestEqual("GetFloatArrayStandardDeviation of an empty array is 0",
UDirectiveUtilMathFunctionLibrary::GetFloatArrayStandardDeviation(EmptyFloats), 0.0f);
}
FRandomStream StreamA(12345);
FRandomStream StreamB(12345);
const int32 StreamIndexA = UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeightsFromStream(StreamA, {1.0f, 1.0f, 1.0f, 1.0f});
const int32 StreamIndexB = UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeightsFromStream(StreamB, {1.0f, 1.0f, 1.0f, 1.0f});
TestEqual("Weighted random from stream is deterministic for the same seed", StreamIndexA, StreamIndexB);
TestTrue("Weighted random from stream returns a valid index", StreamIndexA >= 0 && StreamIndexA < 4);
FRandomStream EmptyStream(1);
TestEqual("Weighted random from stream with all-zero weights returns INDEX_NONE",
UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeightsFromStream(EmptyStream, {0.0f, 0.0f}), static_cast<int32>(INDEX_NONE));
return true;
}

View File

@@ -0,0 +1,78 @@
#include "Libraries/DirectiveUtilRegexFunctionLibrary.h"
#include "Misc/AutomationTest.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilRegexFunctionLibraryTest, "DirectiveUtilities.RegexFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilRegexFunctionLibraryTest::RunTest(const FString& Parameters)
{
TestTrue("RegexMatches should match digits in 'Hello123'", UDirectiveUtilRegexFunctionLibrary::RegexMatches(TEXT("Hello123"), TEXT("[0-9]+")));
TestFalse("RegexMatches should not match digits in 'Hello'", UDirectiveUtilRegexFunctionLibrary::RegexMatches(TEXT("Hello"), TEXT("[0-9]+")));
TestFalse("RegexMatches should be case-sensitive by default", UDirectiveUtilRegexFunctionLibrary::RegexMatches(TEXT("HELLO"), TEXT("hello")));
TestTrue("RegexMatches should honor the case-insensitive flag", UDirectiveUtilRegexFunctionLibrary::RegexMatches(TEXT("HELLO"), TEXT("hello"), false));
TestFalse("RegexMatches should return false for an empty pattern", UDirectiveUtilRegexFunctionLibrary::RegexMatches(TEXT("Hello"), TEXT("")));
{
FString Match;
int32 Start = 0;
int32 End = 0;
const bool bFound = UDirectiveUtilRegexFunctionLibrary::RegexFindFirst(TEXT("abc123def456"), TEXT("[0-9]+"), Match, Start, End);
TestTrue("RegexFindFirst should find a match", bFound);
TestEqual("RegexFindFirst should return the first match", Match, FString(TEXT("123")));
TestEqual("RegexFindFirst should return the match start", Start, 3);
TestEqual("RegexFindFirst should return the match end (exclusive)", End, 6);
}
{
FString Match = TEXT("sentinel");
int32 Start = 5;
int32 End = 5;
const bool bFound = UDirectiveUtilRegexFunctionLibrary::RegexFindFirst(TEXT("abc"), TEXT("[0-9]+"), Match, Start, End);
TestFalse("RegexFindFirst should report no match", bFound);
TestTrue("RegexFindFirst should clear the match on failure", Match.IsEmpty());
TestEqual("RegexFindFirst should set start to INDEX_NONE on failure", Start, static_cast<int32>(INDEX_NONE));
TestEqual("RegexFindFirst should set end to INDEX_NONE on failure", End, static_cast<int32>(INDEX_NONE));
}
TestEqual("RegexFindAll should return every match",
UDirectiveUtilRegexFunctionLibrary::RegexFindAll(TEXT("a1b2c3"), TEXT("[0-9]")),
TArray<FString>({TEXT("1"), TEXT("2"), TEXT("3")}));
TestEqual("RegexFindAll should return nothing when there are no matches",
UDirectiveUtilRegexFunctionLibrary::RegexFindAll(TEXT("abc"), TEXT("[0-9]")).Num(), 0);
TestEqual("RegexFindAll should ignore zero-width matches",
UDirectiveUtilRegexFunctionLibrary::RegexFindAll(TEXT("abc"), TEXT("x*")).Num(), 0);
TestEqual("RegexFindAll should return only the non-empty matches of a star pattern",
UDirectiveUtilRegexFunctionLibrary::RegexFindAll(TEXT("bab"), TEXT("a*")),
TArray<FString>({TEXT("a")}));
TestEqual("RegexReplaceAll should replace every match",
UDirectiveUtilRegexFunctionLibrary::RegexReplaceAll(TEXT("a1b2c3"), TEXT("[0-9]"), TEXT("#")), FString(TEXT("a#b#c#")));
TestEqual("RegexReplaceAll should return the input unchanged when nothing matches",
UDirectiveUtilRegexFunctionLibrary::RegexReplaceAll(TEXT("abc"), TEXT("[0-9]"), TEXT("#")), FString(TEXT("abc")));
TestEqual("RegexReplaceAll should return the input unchanged for an empty pattern",
UDirectiveUtilRegexFunctionLibrary::RegexReplaceAll(TEXT("abc"), TEXT(""), TEXT("#")), FString(TEXT("abc")));
TestEqual("RegexReplaceAll should only replace the non-empty matches of a star pattern",
UDirectiveUtilRegexFunctionLibrary::RegexReplaceAll(TEXT("bab"), TEXT("a*"), TEXT("X")), FString(TEXT("bXb")));
TestEqual("RegexReplaceAll should still replace every non-empty match",
UDirectiveUtilRegexFunctionLibrary::RegexReplaceAll(TEXT("aaa"), TEXT("a"), TEXT("b")), FString(TEXT("bbb")));
{
FString Group;
TestTrue("RegexGetCaptureGroup should return the whole match for group 0",
UDirectiveUtilRegexFunctionLibrary::RegexGetCaptureGroup(TEXT("2024-06-24"), TEXT("([0-9]+)-([0-9]+)-([0-9]+)"), 0, Group));
TestEqual("RegexGetCaptureGroup group 0 should be the whole match", Group, FString(TEXT("2024-06-24")));
TestTrue("RegexGetCaptureGroup should return group 1",
UDirectiveUtilRegexFunctionLibrary::RegexGetCaptureGroup(TEXT("2024-06-24"), TEXT("([0-9]+)-([0-9]+)-([0-9]+)"), 1, Group));
TestEqual("RegexGetCaptureGroup group 1 should be the year", Group, FString(TEXT("2024")));
TestTrue("RegexGetCaptureGroup should return group 2",
UDirectiveUtilRegexFunctionLibrary::RegexGetCaptureGroup(TEXT("2024-06-24"), TEXT("([0-9]+)-([0-9]+)-([0-9]+)"), 2, Group));
TestEqual("RegexGetCaptureGroup group 2 should be the month", Group, FString(TEXT("06")));
Group = TEXT("sentinel");
TestFalse("RegexGetCaptureGroup should fail for a non-existent group",
UDirectiveUtilRegexFunctionLibrary::RegexGetCaptureGroup(TEXT("2024-06-24"), TEXT("([0-9]+)-([0-9]+)-([0-9]+)"), 9, Group));
TestTrue("RegexGetCaptureGroup should clear the output for a non-existent group", Group.IsEmpty());
}
return true;
}

View File

@@ -0,0 +1,122 @@
#include "Libraries/DirectiveUtilSaveGameFunctionLibrary.h"
#include "Tests/DirectiveUtilTestObject.h"
#include "Kismet/GameplayStatics.h"
#include "GameFramework/SaveGame.h"
#include "Misc/AutomationTest.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilSaveGameFunctionLibraryTest, "DirectiveUtilities.SaveGameFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilSaveGameFunctionLibraryTest::RunTest(const FString& Parameters)
{
{
USaveGame* Save = NewObject<UDirectiveUtilTestSaveGame>();
TArray<uint8> Bytes;
const bool bSaved = UDirectiveUtilSaveGameFunctionLibrary::SaveGameToBytes(Save, Bytes);
TestTrue("SaveGameToBytes should succeed for a valid object", bSaved);
TestTrue("SaveGameToBytes should produce non-empty data", Bytes.Num() > 0);
USaveGame* Loaded = UDirectiveUtilSaveGameFunctionLibrary::LoadGameFromBytes(Bytes);
TestNotNull("LoadGameFromBytes should return a valid object", Loaded);
}
{
TArray<uint8> Bytes;
Bytes.Add(1);
TestFalse("SaveGameToBytes should fail for a null object", UDirectiveUtilSaveGameFunctionLibrary::SaveGameToBytes(nullptr, Bytes));
TestEqual("SaveGameToBytes should clear the output for a null object", Bytes.Num(), 0);
TestNull("LoadGameFromBytes should return null for empty data", UDirectiveUtilSaveGameFunctionLibrary::LoadGameFromBytes(TArray<uint8>()));
}
{
const FString TestSlot = TEXT("DirectiveUtilitiesAutomationTestSlot");
UGameplayStatics::DeleteGameInSlot(TestSlot, 0);
USaveGame* Save = NewObject<UDirectiveUtilTestSaveGame>();
const bool bWritten = UGameplayStatics::SaveGameToSlot(Save, TestSlot, 0);
TestTrue("SaveGameToSlot should write the temporary test slot", bWritten);
if (bWritten)
{
TestTrue("GetAllSaveSlotNames should include the written slot",
UDirectiveUtilSaveGameFunctionLibrary::GetAllSaveSlotNames().Contains(TestSlot));
FDateTime Timestamp;
TestTrue("GetSaveSlotTimestamp should succeed for an existing slot",
UDirectiveUtilSaveGameFunctionLibrary::GetSaveSlotTimestamp(TestSlot, Timestamp));
TestTrue("GetSaveSlotTimestamp should return local time for a fresh save",
FMath::Abs((FDateTime::Now() - Timestamp).GetTotalMinutes()) < 5.0);
}
// Clean up
UGameplayStatics::DeleteGameInSlot(TestSlot, 0);
TestFalse("A deleted slot should no longer be listed",
UDirectiveUtilSaveGameFunctionLibrary::GetAllSaveSlotNames().Contains(TestSlot));
FDateTime MissingTimestamp;
TestFalse("GetSaveSlotTimestamp should fail for a missing slot",
UDirectiveUtilSaveGameFunctionLibrary::GetSaveSlotTimestamp(TestSlot, MissingTimestamp));
}
{
const FString SlotA = TEXT("DirectiveUtilitiesAutomationTestSlotA");
const FString SlotB = TEXT("DirectiveUtilitiesAutomationTestSlotB");
const FString SlotC = TEXT("DirectiveUtilitiesAutomationTestSlotC");
UGameplayStatics::DeleteGameInSlot(SlotA, 0);
UGameplayStatics::DeleteGameInSlot(SlotB, 0);
UGameplayStatics::DeleteGameInSlot(SlotC, 0);
USaveGame* Save = NewObject<UDirectiveUtilTestSaveGame>();
const bool bWritten = UGameplayStatics::SaveGameToSlot(Save, SlotA, 0);
TestTrue("SaveGameToSlot should write the slot-management test slot", bWritten);
if (bWritten)
{
TestTrue("DoesSaveSlotExist should find the written slot",
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(SlotA));
TestFalse("DoesSaveSlotExist should not find a missing slot",
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(SlotB));
TestTrue("RenameSaveSlot should rename onto a free slot name",
UDirectiveUtilSaveGameFunctionLibrary::RenameSaveSlot(SlotA, SlotB));
TestFalse("RenameSaveSlot should remove the old slot",
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(SlotA));
TestTrue("RenameSaveSlot should create the new slot",
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(SlotB));
TestNotNull("A renamed slot should still deserialize",
UGameplayStatics::LoadGameFromSlot(SlotB, 0));
USaveGame* OtherSave = NewObject<UDirectiveUtilTestSaveGame>();
TestTrue("SaveGameToSlot should write the collision test slot",
UGameplayStatics::SaveGameToSlot(OtherSave, SlotC, 0));
TestFalse("RenameSaveSlot should refuse to rename onto an existing slot",
UDirectiveUtilSaveGameFunctionLibrary::RenameSaveSlot(SlotB, SlotC));
TestTrue("A refused rename should keep the source slot",
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(SlotB));
TestTrue("A refused rename should keep the target slot",
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(SlotC));
TestFalse("DoesSaveSlotExist should reject an invalid slot name",
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(TEXT("../escape")));
TestFalse("DeleteSaveSlot should reject an invalid slot name",
UDirectiveUtilSaveGameFunctionLibrary::DeleteSaveSlot(TEXT("../escape")));
TestFalse("RenameSaveSlot should reject an invalid source slot name",
UDirectiveUtilSaveGameFunctionLibrary::RenameSaveSlot(TEXT("../escape"), SlotC));
TestFalse("RenameSaveSlot should reject an invalid destination slot name",
UDirectiveUtilSaveGameFunctionLibrary::RenameSaveSlot(SlotC, TEXT("../escape")));
TestTrue("A rejected rename should leave the source slot intact",
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(SlotC));
TestTrue("DeleteSaveSlot should delete an existing slot",
UDirectiveUtilSaveGameFunctionLibrary::DeleteSaveSlot(SlotB));
TestFalse("A deleted slot should no longer exist",
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(SlotB));
}
// Clean up
UGameplayStatics::DeleteGameInSlot(SlotA, 0);
UGameplayStatics::DeleteGameInSlot(SlotB, 0);
UGameplayStatics::DeleteGameInSlot(SlotC, 0);
}
return true;
}

View File

@@ -0,0 +1,277 @@
#include "Libraries/DirectiveUtilStringFunctionLibrary.h"
#include "Misc/AutomationTest.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilStringFunctionLibraryTest, "DirectiveUtilities.StringFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilStringFunctionLibraryTest::RunTest(const FString& Parameters)
{
TestTrue("ContainsLetters should return true for 'Hello'", UDirectiveUtilStringFunctionLibrary::ContainsLetters(TEXT("Hello")));
TestFalse("ContainsLetters should return false for '1234'", UDirectiveUtilStringFunctionLibrary::ContainsLetters(TEXT("1234")));
TestTrue("ContainsNumbers should return true for '1234'", UDirectiveUtilStringFunctionLibrary::ContainsNumbers(TEXT("1234")));
TestFalse("ContainsNumbers should return false for 'Hello'", UDirectiveUtilStringFunctionLibrary::ContainsNumbers(TEXT("Hello")));
TestTrue("ContainsSpaces should return true for 'Hello World'", UDirectiveUtilStringFunctionLibrary::ContainsSpaces(TEXT("Hello World")));
TestFalse("ContainsSpaces should return false for 'HelloWorld'", UDirectiveUtilStringFunctionLibrary::ContainsSpaces(TEXT("HelloWorld")));
TestTrue("ContainsSpecialCharacters should return true for 'Hello!'", UDirectiveUtilStringFunctionLibrary::ContainsSpecialCharacters(TEXT("Hello!")));
TestFalse("ContainsSpecialCharacters should return false for 'Hello'", UDirectiveUtilStringFunctionLibrary::ContainsSpecialCharacters(TEXT("Hello")));
const FString FilteredString = UDirectiveUtilStringFunctionLibrary::FilterCharacters(TEXT("Hello123! "), true, true, true, true);
TestEqual("FilterCharacters should return an empty string", FilteredString, TEXT(""));
const TArray<FString> UnsortedArray = { TEXT("Banana"), TEXT("Apple"), TEXT("Cherry") };
const TArray<FString> SortedArray = UDirectiveUtilStringFunctionLibrary::GetSortedStringArray(UnsortedArray);
TestEqual("GetSortedStringArray should return a sorted array", SortedArray, TArray<FString>({ TEXT("Apple"), TEXT("Banana"), TEXT("Cherry") }));
TestEqual("TruncateString should not truncate short strings",
UDirectiveUtilStringFunctionLibrary::TruncateString(TEXT("Hello"), 10), FString(TEXT("Hello")));
TestEqual("TruncateString should truncate long strings with suffix",
UDirectiveUtilStringFunctionLibrary::TruncateString(TEXT("Hello World"), 8), FString(TEXT("Hello...")));
TestEqual("TruncateString should not truncate strings at exact max length",
UDirectiveUtilStringFunctionLibrary::TruncateString(TEXT("Hello"), 5), FString(TEXT("Hello")));
TestEqual("ToTitleCase should capitalize first letter of each word",
UDirectiveUtilStringFunctionLibrary::ToTitleCase(TEXT("hello world")), FString(TEXT("Hello World")));
TestEqual("ToTitleCase should handle all caps",
UDirectiveUtilStringFunctionLibrary::ToTitleCase(TEXT("HELLO WORLD")), FString(TEXT("Hello World")));
TestEqual("ToTitleCase should handle single word",
UDirectiveUtilStringFunctionLibrary::ToTitleCase(TEXT("hello")), FString(TEXT("Hello")));
TestEqual("ToTitleCase should handle empty string",
UDirectiveUtilStringFunctionLibrary::ToTitleCase(TEXT("")), FString(TEXT("")));
// When MaxLength < Suffix.Len(), MaxLength - Suffix.Len() is negative; FString::Left clamps
// negative counts to 0, so the result is just the suffix (no crash).
TestEqual("TruncateString should return only the suffix when MaxLength is less than the suffix length",
UDirectiveUtilStringFunctionLibrary::TruncateString(TEXT("Hello World"), 2), FString(TEXT("...")));
TestEqual("TruncateString should return only the suffix when MaxLength is zero",
UDirectiveUtilStringFunctionLibrary::TruncateString(TEXT("Hello World"), 0), FString(TEXT("...")));
TestEqual("TruncateString should return only the suffix when MaxLength equals the suffix length",
UDirectiveUtilStringFunctionLibrary::TruncateString(TEXT("Hello World"), 3), FString(TEXT("...")));
TestEqual("TruncateString should not truncate an empty input string",
UDirectiveUtilStringFunctionLibrary::TruncateString(TEXT(""), 5), FString(TEXT("")));
TestEqual("TruncateString should honor a custom suffix",
UDirectiveUtilStringFunctionLibrary::TruncateString(TEXT("Hello World"), 6, TEXT(">>")), FString(TEXT("Hell>>")));
TestEqual("TruncateString should return only a custom suffix when MaxLength is less than its length",
UDirectiveUtilStringFunctionLibrary::TruncateString(TEXT("Hello World"), 1, TEXT(">>")), FString(TEXT(">>")));
TestEqual("TruncateString should handle an empty suffix by returning a hard cut",
UDirectiveUtilStringFunctionLibrary::TruncateString(TEXT("Hello World"), 5, TEXT("")), FString(TEXT("Hello")));
TestFalse("ContainsLetters should return false for an empty string", UDirectiveUtilStringFunctionLibrary::ContainsLetters(TEXT("")));
TestFalse("ContainsNumbers should return false for an empty string", UDirectiveUtilStringFunctionLibrary::ContainsNumbers(TEXT("")));
TestFalse("ContainsSpaces should return false for an empty string", UDirectiveUtilStringFunctionLibrary::ContainsSpaces(TEXT("")));
TestFalse("ContainsSpecialCharacters should return false for an empty string", UDirectiveUtilStringFunctionLibrary::ContainsSpecialCharacters(TEXT("")));
TestTrue("ContainsSpaces should treat a tab as whitespace", UDirectiveUtilStringFunctionLibrary::ContainsSpaces(TEXT("Hello\tWorld")));
TestFalse("ContainsSpecialCharacters should return false for letters and digits only", UDirectiveUtilStringFunctionLibrary::ContainsSpecialCharacters(TEXT("Hello123")));
TestEqual("FilterCharacters should strip only letters",
UDirectiveUtilStringFunctionLibrary::FilterCharacters(TEXT("Hello123! "), true, false, false, false), FString(TEXT("123! ")));
TestEqual("FilterCharacters should strip only numbers",
UDirectiveUtilStringFunctionLibrary::FilterCharacters(TEXT("Hello123! "), false, true, false, false), FString(TEXT("Hello! ")));
TestEqual("FilterCharacters should strip only special characters",
UDirectiveUtilStringFunctionLibrary::FilterCharacters(TEXT("Hello123! "), false, false, true, false), FString(TEXT("Hello123 ")));
TestEqual("FilterCharacters should strip only spaces",
UDirectiveUtilStringFunctionLibrary::FilterCharacters(TEXT("Hello123! "), false, false, false, true), FString(TEXT("Hello123!")));
TestEqual("FilterCharacters should return the original string when no flags are set",
UDirectiveUtilStringFunctionLibrary::FilterCharacters(TEXT("Hello123! "), false, false, false, false), FString(TEXT("Hello123! ")));
TestEqual("FilterCharacters should return an empty string for an empty input",
UDirectiveUtilStringFunctionLibrary::FilterCharacters(TEXT(""), true, true, true, true), FString(TEXT("")));
{
const FString EquivalentInputs[] = {
TEXT("my var name"), TEXT("my_var_name"), TEXT("my-var-name"), TEXT("MyVarName"), TEXT("myVarName")
};
for (const FString& Input : EquivalentInputs)
{
TestEqual(FString::Printf(TEXT("ToCamelCase(\"%s\") is \"myVarName\""), *Input),
UDirectiveUtilStringFunctionLibrary::ToCamelCase(Input), FString(TEXT("myVarName")));
TestEqual(FString::Printf(TEXT("ToPascalCase(\"%s\") is \"MyVarName\""), *Input),
UDirectiveUtilStringFunctionLibrary::ToPascalCase(Input), FString(TEXT("MyVarName")));
TestEqual(FString::Printf(TEXT("ToSnakeCase(\"%s\") is \"my_var_name\""), *Input),
UDirectiveUtilStringFunctionLibrary::ToSnakeCase(Input), FString(TEXT("my_var_name")));
TestEqual(FString::Printf(TEXT("ToKebabCase(\"%s\") is \"my-var-name\""), *Input),
UDirectiveUtilStringFunctionLibrary::ToKebabCase(Input), FString(TEXT("my-var-name")));
}
TestEqual("SplitIntoWords keeps acronyms together",
UDirectiveUtilStringFunctionLibrary::SplitIntoWords(TEXT("XMLParser")),
TArray<FString>({ TEXT("XML"), TEXT("Parser") }));
TestEqual("SplitIntoWords splits on letter/digit transitions",
UDirectiveUtilStringFunctionLibrary::SplitIntoWords(TEXT("version2Beta")),
TArray<FString>({ TEXT("version"), TEXT("2"), TEXT("Beta") }));
TestEqual("SplitIntoWords of an empty string is an empty array",
UDirectiveUtilStringFunctionLibrary::SplitIntoWords(TEXT("")), TArray<FString>());
TestEqual("ToCamelCase(\"XMLParser\") is \"xmlParser\"",
UDirectiveUtilStringFunctionLibrary::ToCamelCase(TEXT("XMLParser")), FString(TEXT("xmlParser")));
TestEqual("ToSnakeCase(\"XMLParser\") is \"xml_parser\"",
UDirectiveUtilStringFunctionLibrary::ToSnakeCase(TEXT("XMLParser")), FString(TEXT("xml_parser")));
TestEqual("ToSnakeCase(\"version2Beta\") is \"version_2_beta\"",
UDirectiveUtilStringFunctionLibrary::ToSnakeCase(TEXT("version2Beta")), FString(TEXT("version_2_beta")));
TestEqual("ToCamelCase of an empty string is empty",
UDirectiveUtilStringFunctionLibrary::ToCamelCase(TEXT("")), FString(TEXT("")));
TestEqual("ToPascalCase of an empty string is empty",
UDirectiveUtilStringFunctionLibrary::ToPascalCase(TEXT("")), FString(TEXT("")));
TestEqual("ToSnakeCase of an empty string is empty",
UDirectiveUtilStringFunctionLibrary::ToSnakeCase(TEXT("")), FString(TEXT("")));
TestEqual("ToKebabCase of an empty string is empty",
UDirectiveUtilStringFunctionLibrary::ToKebabCase(TEXT("")), FString(TEXT("")));
}
const TArray<FString> UnsortedForLegacy = { TEXT("Banana"), TEXT("Apple"), TEXT("Cherry") };
TestEqual("SortStringArray should return a sorted array",
UDirectiveUtilStringFunctionLibrary::SortStringArray(UnsortedForLegacy),
TArray<FString>({ TEXT("Apple"), TEXT("Banana"), TEXT("Cherry") }));
TestEqual("SortStringArray should return an empty array for empty input",
UDirectiveUtilStringFunctionLibrary::SortStringArray(TArray<FString>()), TArray<FString>());
TestEqual("GetSortedStringArray should return an empty array for empty input",
UDirectiveUtilStringFunctionLibrary::GetSortedStringArray(TArray<FString>()), TArray<FString>());
TestEqual("Levenshtein of identical strings is 0",
UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(TEXT("abc"), TEXT("abc")), 0);
TestEqual("Levenshtein kitten->sitting is 3",
UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(TEXT("kitten"), TEXT("sitting")), 3);
TestEqual("Levenshtein from empty equals the other length",
UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(TEXT(""), TEXT("abc")), 3);
TestEqual("Levenshtein is case-sensitive by default",
UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(TEXT("ABC"), TEXT("abc")), 3);
TestEqual("Levenshtein honors the case-insensitive flag",
UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(TEXT("ABC"), TEXT("abc"), /*bCaseSensitive*/ false), 0);
TestTrue("Similarity of identical strings is 1",
FMath::IsNearlyEqual(UDirectiveUtilStringFunctionLibrary::GetStringSimilarity(TEXT("abc"), TEXT("abc")), 1.0f, 1.e-4f));
TestTrue("Similarity of two empty strings is 1",
FMath::IsNearlyEqual(UDirectiveUtilStringFunctionLibrary::GetStringSimilarity(TEXT(""), TEXT("")), 1.0f, 1.e-4f));
TestTrue("Similarity of completely different equal-length strings is 0",
FMath::IsNearlyEqual(UDirectiveUtilStringFunctionLibrary::GetStringSimilarity(TEXT("abc"), TEXT("xyz")), 0.0f, 1.e-4f));
TestTrue("Similarity kitten/sitting is ~0.571",
FMath::IsNearlyEqual(UDirectiveUtilStringFunctionLibrary::GetStringSimilarity(TEXT("kitten"), TEXT("sitting")), 1.0f - 3.0f / 7.0f, 1.e-3f));
TestTrue("ContainsAny finds a present term",
UDirectiveUtilStringFunctionLibrary::ContainsAny(TEXT("Hello World"), {TEXT("foo"), TEXT("World")}));
TestFalse("ContainsAny returns false when no term is present",
UDirectiveUtilStringFunctionLibrary::ContainsAny(TEXT("Hello"), {TEXT("foo"), TEXT("bar")}));
TestTrue("ContainsAny honors the case-insensitive flag",
UDirectiveUtilStringFunctionLibrary::ContainsAny(TEXT("HELLO"), {TEXT("hello")}, /*bCaseSensitive*/ false));
TestFalse("ContainsAny ignores empty terms",
UDirectiveUtilStringFunctionLibrary::ContainsAny(TEXT("Hello"), {TEXT("")}));
{
int32 FoundIndex = 0;
int32 TermIndex = 0;
const bool bFound = UDirectiveUtilStringFunctionLibrary::FindFirstOfAny(TEXT("abcXYZ123"), {TEXT("123"), TEXT("XYZ")}, true, FoundIndex, TermIndex);
TestTrue("FindFirstOfAny should find a term", bFound);
TestEqual("FindFirstOfAny should return the earliest match index", FoundIndex, 3);
TestEqual("FindFirstOfAny should return which term matched earliest", TermIndex, 1);
}
{
int32 FoundIndex = 5;
int32 TermIndex = 5;
const bool bFound = UDirectiveUtilStringFunctionLibrary::FindFirstOfAny(TEXT("abc"), {TEXT("x"), TEXT("y")}, true, FoundIndex, TermIndex);
TestFalse("FindFirstOfAny should report no match", bFound);
TestEqual("FindFirstOfAny should set found index to INDEX_NONE on failure", FoundIndex, static_cast<int32>(INDEX_NONE));
TestEqual("FindFirstOfAny should set term index to INDEX_NONE on failure", TermIndex, static_cast<int32>(INDEX_NONE));
}
{
const FString Original = TEXT("Hello, DirectiveUtilities! 123");
const FString Encoded = UDirectiveUtilStringFunctionLibrary::Base64Encode(Original);
TestFalse("Base64Encode should produce a non-empty string", Encoded.IsEmpty());
FString Decoded;
const bool bDecoded = UDirectiveUtilStringFunctionLibrary::Base64Decode(Encoded, Decoded);
TestTrue("Base64Decode should succeed for valid input", bDecoded);
TestEqual("Base64 should round-trip the original string", Decoded, Original);
}
{
TestEqual("HexEncode(\"Hi!\") is \"486921\"",
UDirectiveUtilStringFunctionLibrary::HexEncode(TEXT("Hi!")), FString(TEXT("486921")));
TestEqual("HexEncode of an empty string is empty",
UDirectiveUtilStringFunctionLibrary::HexEncode(TEXT("")), FString(TEXT("")));
FString Decoded;
TestTrue("HexDecode should decode valid hex", UDirectiveUtilStringFunctionLibrary::HexDecode(TEXT("486921"), Decoded));
TestEqual("HexDecode should round-trip HexEncode", Decoded, FString(TEXT("Hi!")));
TestTrue("HexDecode should accept uppercase hex", UDirectiveUtilStringFunctionLibrary::HexDecode(TEXT("4A"), Decoded));
TestEqual("HexDecode of \"4A\" is \"J\"", Decoded, FString(TEXT("J")));
TestFalse("HexDecode should reject non-hex characters", UDirectiveUtilStringFunctionLibrary::HexDecode(TEXT("XYZ"), Decoded));
TestFalse("HexDecode should reject odd-length input", UDirectiveUtilStringFunctionLibrary::HexDecode(TEXT("48692"), Decoded));
TestEqual("HexEncodeBytes({0x00, 0xFF}) is \"00ff\"",
UDirectiveUtilStringFunctionLibrary::HexEncodeBytes({0x00, 0xFF}), FString(TEXT("00ff")));
TArray<uint8> DecodedBytes;
TestTrue("HexDecodeBytes should decode valid hex", UDirectiveUtilStringFunctionLibrary::HexDecodeBytes(TEXT("00ff"), DecodedBytes));
TestEqual("HexDecodeBytes should round-trip HexEncodeBytes", DecodedBytes, TArray<uint8>({0x00, 0xFF}));
TestFalse("HexDecodeBytes should reject non-hex characters", UDirectiveUtilStringFunctionLibrary::HexDecodeBytes(TEXT("zz"), DecodedBytes));
}
TestEqual("Md5HashString(\"abc\") matches the known vector",
UDirectiveUtilStringFunctionLibrary::Md5HashString(TEXT("abc")), FString(TEXT("900150983cd24fb0d6963f7d28e17f72")));
TestEqual("Md5HashString of an empty string matches the known vector",
UDirectiveUtilStringFunctionLibrary::Md5HashString(TEXT("")), FString(TEXT("d41d8cd98f00b204e9800998ecf8427e")));
TestEqual("Sha1HashString(\"abc\") matches the known vector",
UDirectiveUtilStringFunctionLibrary::Sha1HashString(TEXT("abc")), FString(TEXT("a9993e364706816aba3e25717850c26c9cd0d89d")));
TestEqual("Sha1HashBytes of {0x61,0x62,0x63} matches the known vector",
UDirectiveUtilStringFunctionLibrary::Sha1HashBytes({0x61, 0x62, 0x63}), FString(TEXT("a9993e364706816aba3e25717850c26c9cd0d89d")));
TestEqual("Crc32String(\"abc\") equals Crc32Bytes of its ASCII bytes",
UDirectiveUtilStringFunctionLibrary::Crc32String(TEXT("abc")),
UDirectiveUtilStringFunctionLibrary::Crc32Bytes({0x61, 0x62, 0x63}));
TestEqual("Crc32String of an empty string is 0",
UDirectiveUtilStringFunctionLibrary::Crc32String(TEXT("")), 0);
TestEqual("Md5HashString hashes the UTF-8 bytes of non-ASCII input",
UDirectiveUtilStringFunctionLibrary::Md5HashString(TEXT("\u00E9")), // e-acute; UTF-8 bytes C3 A9
UDirectiveUtilStringFunctionLibrary::Md5HashBytes({0xC3, 0xA9}));
TestTrue("IsValidFileName should accept a simple name", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("Save01")));
TestTrue("IsValidFileName should accept a name with a space and extension", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("my save.sav")));
TestFalse("IsValidFileName should reject a relative segment", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("../escape")));
TestFalse("IsValidFileName should reject a forward slash", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("a/b")));
TestFalse("IsValidFileName should reject a backslash", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("a\\b")));
TestFalse("IsValidFileName should reject an empty string", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("")));
TestFalse("IsValidFileName should reject a reserved character", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("a:b")));
{
const FString Sanitized = UDirectiveUtilStringFunctionLibrary::SanitizeFileName(TEXT("../a/b?.sav"));
TestTrue("SanitizeFileName should produce a name that IsValidFileName accepts",
UDirectiveUtilStringFunctionLibrary::IsValidFileName(Sanitized));
const FString Replaced = UDirectiveUtilStringFunctionLibrary::SanitizeFileName(TEXT("a/b"), TEXT("_"));
TestTrue("SanitizeFileName should substitute the replacement character for stripped characters",
Replaced.Contains(TEXT("_")));
TestTrue("SanitizeFileName with a replacement should still produce a valid file name",
UDirectiveUtilStringFunctionLibrary::IsValidFileName(Replaced));
}
{
float Similarity = -1.0f;
TestEqual("FindBestStringMatch should return the exact match",
UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(TEXT("color"), {TEXT("colour"), TEXT("color")}, Similarity), 1);
TestTrue("FindBestStringMatch should report similarity 1 for an exact match",
FMath::IsNearlyEqual(Similarity, 1.0f, 1.e-4f));
TestEqual("FindBestStringMatch should return the nearest match",
UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(TEXT("colr"), {TEXT("color"), TEXT("colour"), TEXT("colt")}, Similarity), 0);
TestEqual("FindBestStringMatch should be case-insensitive by default",
UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(TEXT("COLOR"), {TEXT("apple"), TEXT("color")}, Similarity), 1);
TestTrue("FindBestStringMatch should report similarity 1 for a case-insensitive match",
FMath::IsNearlyEqual(Similarity, 1.0f, 1.e-4f));
TestEqual("FindBestStringMatch should honor the case-sensitive flag",
UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(TEXT("COLOR"), {TEXT("color"), TEXT("COLT")}, Similarity, /*bCaseSensitive*/ true), 1);
TestEqual("FindBestStringMatch should return -1 for an empty array",
UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(TEXT("color"), TArray<FString>(), Similarity), static_cast<int32>(INDEX_NONE));
TestTrue("FindBestStringMatch should report similarity 0 for an empty array",
FMath::IsNearlyEqual(Similarity, 0.0f, 1.e-4f));
TestEqual("FindBestStringMatch should still return an index for an empty input",
UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(TEXT(""), {TEXT("abc")}, Similarity), 0);
TestTrue("FindBestStringMatch should report similarity 0 for an empty input against a non-empty candidate",
FMath::IsNearlyEqual(Similarity, 0.0f, 1.e-4f));
}
return true;
}

View File

@@ -0,0 +1,15 @@
#include "Libraries/DirectiveUtilTextFunctionLibrary.h"
#include "Misc/AutomationTest.h"
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilTextFunctionLibraryTest, "DirectiveUtilities.TextFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FDirectiveUtilTextFunctionLibraryTest::RunTest(const FString& Parameters)
{
const FText NonEmptyText = FText::FromString(TEXT("Hello"));
TestTrue("IsNotEmpty should return true for non-empty text", UDirectiveUtilTextFunctionLibrary::IsNotEmpty(NonEmptyText));
const FText EmptyText = FText::GetEmpty();
TestFalse("IsNotEmpty should return false for empty text", UDirectiveUtilTextFunctionLibrary::IsNotEmpty(EmptyText));
return true;
}

View File

@@ -0,0 +1,11 @@
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
class FDirectiveUtilitiesTestsModule : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};

View File

@@ -0,0 +1,115 @@
#pragma once
#include "CoreMinimal.h"
#include "UObject/Object.h"
#include "GameFramework/SaveGame.h"
#include "Engine/HitResult.h"
#include "DirectiveUtilTestObject.generated.h"
class UWorld;
UCLASS()
class UDirectiveUtilTestObject : public UObject
{
GENERATED_BODY()
public:
UPROPERTY()
TArray<int32> TestArray;
UPROPERTY()
TMap<int32, int32> TestMap;
UPROPERTY()
TMap<int32, int32> TestMap2;
UPROPERTY()
TMap<FString, int32> TestStringKeyMap;
};
/** Concrete save-game subclass for tests (USaveGame itself is abstract and cannot be instantiated). */
UCLASS()
class UDirectiveUtilTestSaveGame : public USaveGame
{
GENERATED_BODY()
public:
UPROPERTY()
int32 TestValue = 0;
};
/**
* Listener for exercising the async-task dynamic multicast delegates from automation tests.
* Dynamic delegates can only bind to UFUNCTIONs, so the handlers live on a UObject. The
* Keepalive property lets a rooted listener keep the async task itself alive across frames.
*/
UCLASS()
class UDirectiveUtilDelegateListener : public UObject
{
GENERATED_BODY()
public:
UPROPERTY()
bool bCompleted = false;
UPROPERTY()
bool bFailed = false;
UPROPERTY()
int32 CompletedCount = 0;
/** Number of hits reported by the most recent trace completion. */
UPROPERTY()
int32 HitCount = 0;
/** Success flag from the most recent bool-payload completion (e.g. move-to-location). */
UPROPERTY()
bool bLastSuccess = false;
UPROPERTY()
TObjectPtr<UObject> LastObject = nullptr;
/** The object array from the most recent batch completion (e.g. async load assets). */
UPROPERTY()
TArray<TObjectPtr<UObject>> LastObjects;
/** Holds a strong reference to the async task so it survives GC while the test waits. */
UPROPERTY()
TObjectPtr<UObject> Keepalive = nullptr;
/** The transient world a latent delay scenario ticks and tears down when it settles. */
UPROPERTY()
TObjectPtr<UWorld> ScenarioWorld = nullptr;
/** Handler for parameterless completion delegates (e.g. the cancellable delay). */
UFUNCTION()
void OnCompleted() { bCompleted = true; ++CompletedCount; }
/** Handler for object-payload completion delegates (e.g. async load asset). */
UFUNCTION()
void OnObjectCompleted(UObject* Object) { bCompleted = true; ++CompletedCount; LastObject = Object; }
/** Handler for object-payload failure delegates. */
UFUNCTION()
void OnObjectFailed(UObject* Object) { bFailed = true; }
/** Handler for object-array-payload completion delegates (e.g. async load assets). */
UFUNCTION()
void OnObjectsCompleted(const TArray<UObject*>& Objects) { bCompleted = true; ++CompletedCount; LastObjects.Reset(); LastObjects.Append(Objects); }
/** Handler for class-payload completion delegates (e.g. async load class). */
UFUNCTION()
void OnClassCompleted(UClass* Class) { bCompleted = true; ++CompletedCount; LastObject = Class; }
/** Handler for class-payload failure delegates. */
UFUNCTION()
void OnClassFailed(UClass* Class) { bFailed = true; }
/** Handler for trace completion delegates. */
UFUNCTION()
void OnTraceCompleted(const TArray<FHitResult>& Hits) { bCompleted = true; ++CompletedCount; HitCount = Hits.Num(); }
/** Handler for bool-payload completion delegates (e.g. move-to-location). */
UFUNCTION()
void OnBoolCompleted(bool bSuccess) { bCompleted = true; ++CompletedCount; bLastSuccess = bSuccess; }
};