Import helper plugin
This commit is contained in:
@@ -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")
|
||||||
13
Plugins/DirectiveUtilities/Config/FilterPlugin.ini
Normal file
13
Plugins/DirectiveUtilities/Config/FilterPlugin.ini
Normal 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
|
||||||
60
Plugins/DirectiveUtilities/DirectiveUtilities.uplugin
Normal file
60
Plugins/DirectiveUtilities/DirectiveUtilities.uplugin
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
21
Plugins/DirectiveUtilities/LICENSE
Normal file
21
Plugins/DirectiveUtilities/LICENSE
Normal 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.
|
||||||
BIN
Plugins/DirectiveUtilities/Resources/Icon128.png
LFS
Normal file
BIN
Plugins/DirectiveUtilities/Resources/Icon128.png
LFS
Normal file
Binary file not shown.
10
Plugins/DirectiveUtilities/Resources/UDCoreIcon.svg
Normal file
10
Plugins/DirectiveUtilities/Resources/UDCoreIcon.svg
Normal 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 |
@@ -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",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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>();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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."),
|
||||||
|
};
|
||||||
@@ -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[]
|
||||||
|
{
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#include "DirectiveUtilLogChannels.h"
|
||||||
|
|
||||||
|
DEFINE_LOG_CATEGORY(LogDirectiveUtil);
|
||||||
|
DEFINE_LOG_CATEGORY(LogDirectiveUtilEditor);
|
||||||
@@ -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)
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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();
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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."),
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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; }
|
||||||
|
};
|
||||||
@@ -29,7 +29,8 @@ public class ProjectEleri : ModuleRules
|
|||||||
"GameplayTasks",
|
"GameplayTasks",
|
||||||
"UniversalCameraPlugin",
|
"UniversalCameraPlugin",
|
||||||
"StateTreeModule",
|
"StateTreeModule",
|
||||||
"GameplayStateTreeModule"
|
"GameplayStateTreeModule",
|
||||||
|
"DirectiveUtilitiesRuntime"
|
||||||
});
|
});
|
||||||
|
|
||||||
PrivateDependencyModuleNames.AddRange(new string[] { "OnlineSubsystem", "OnlineSubsystemUtils" });
|
PrivateDependencyModuleNames.AddRange(new string[] { "OnlineSubsystem", "OnlineSubsystemUtils" });
|
||||||
|
|||||||
Reference in New Issue
Block a user