Update directive utilities plugin
This commit is contained in:
Binary file not shown.
@@ -6,8 +6,5 @@
|
|||||||
; /README.txt
|
; /README.txt
|
||||||
; /Extras/...
|
; /Extras/...
|
||||||
; /Binaries/ThirdParty/*.dll
|
; /Binaries/ThirdParty/*.dll
|
||||||
/README.md
|
|
||||||
/LICENSE
|
|
||||||
/CHANGELOG.md
|
|
||||||
/Documentation/...
|
/Documentation/...
|
||||||
/Config/DefaultDirectiveUtilities.ini
|
/Config/DefaultDirectiveUtilities.ini
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
{
|
{
|
||||||
"FileVersion": 3,
|
"FileVersion": 3,
|
||||||
"Version": 3,
|
"Version": 7,
|
||||||
"VersionName": "2.0",
|
"VersionName": "2.2.0",
|
||||||
"FriendlyName": "Directive Utilities",
|
"FriendlyName": "Directive Utilities",
|
||||||
"Description": "An open-source Unreal Engine plugin that provides runtime and editor utility nodes for developers.",
|
"Description": "An open-source Unreal Engine plugin that provides runtime and editor utility nodes for developers.",
|
||||||
"Category": "Unreal Directive",
|
"Category": "Unreal Directive",
|
||||||
"CreatedBy": "Unreal Directive",
|
"CreatedBy": "Unreal Directive",
|
||||||
"CreatedByURL": "https://unrealdirective.com",
|
"CreatedByURL": "https://unrealdirective.com",
|
||||||
"DocsURL": "https://udcore.unrealdirective.com/",
|
"DocsURL": "https://github.com/UnrealDirective/DirectiveUtilities/tree/main/Documentation",
|
||||||
"MarketplaceURL": "https://unrealdirective.com",
|
"MarketplaceURL": "",
|
||||||
"SupportURL": "https://github.com/UnrealDirective/DirectiveUtilities/issues",
|
"SupportURL": "https://github.com/UnrealDirective/DirectiveUtilities/issues",
|
||||||
|
"EngineVersion": "5.8.0",
|
||||||
"CanContainContent": false,
|
"CanContainContent": false,
|
||||||
"IsBetaVersion": false,
|
"IsBetaVersion": false,
|
||||||
"IsExperimentalVersion": false,
|
"IsExperimentalVersion": false,
|
||||||
"Installed": false,
|
"Installed": true,
|
||||||
|
"SupportedTargetPlatforms": [
|
||||||
|
"Win64",
|
||||||
|
"Mac",
|
||||||
|
"Linux"
|
||||||
|
],
|
||||||
"Modules": [
|
"Modules": [
|
||||||
{
|
{
|
||||||
"Name": "DirectiveUtilitiesRuntime",
|
"Name": "DirectiveUtilitiesRuntime",
|
||||||
@@ -25,10 +31,25 @@
|
|||||||
"Linux"
|
"Linux"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"Name": "DirectiveUtilitiesBlueprintNodes",
|
||||||
|
"Type": "UncookedOnly",
|
||||||
|
"LoadingPhase": "Default",
|
||||||
|
"PlatformAllowList": [
|
||||||
|
"Win64",
|
||||||
|
"Mac",
|
||||||
|
"Linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Name": "DirectiveUtilitiesEditor",
|
"Name": "DirectiveUtilitiesEditor",
|
||||||
"Type": "Editor",
|
"Type": "Editor",
|
||||||
"LoadingPhase": "Default",
|
"LoadingPhase": "Default",
|
||||||
|
"PlatformAllowList": [
|
||||||
|
"Win64",
|
||||||
|
"Mac",
|
||||||
|
"Linux"
|
||||||
|
],
|
||||||
"TargetAllowList": [
|
"TargetAllowList": [
|
||||||
"Editor"
|
"Editor"
|
||||||
]
|
]
|
||||||
@@ -50,7 +71,10 @@
|
|||||||
"Plugins": [
|
"Plugins": [
|
||||||
{
|
{
|
||||||
"Name": "EditorScriptingUtilities",
|
"Name": "EditorScriptingUtilities",
|
||||||
"Enabled": true
|
"Enabled": true,
|
||||||
|
"TargetAllowList": [
|
||||||
|
"Editor"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Name": "EnhancedInput",
|
"Name": "EnhancedInput",
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
using UnrealBuildTool;
|
||||||
|
|
||||||
|
public class DirectiveUtilitiesBlueprintNodes : ModuleRules
|
||||||
|
{
|
||||||
|
public DirectiveUtilitiesBlueprintNodes(ReadOnlyTargetRules Target) : base(Target)
|
||||||
|
{
|
||||||
|
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||||
|
|
||||||
|
PublicDependencyModuleNames.AddRange(
|
||||||
|
new string[]
|
||||||
|
{
|
||||||
|
"BlueprintGraph",
|
||||||
|
"Core",
|
||||||
|
"CoreUObject",
|
||||||
|
"Engine",
|
||||||
|
"DirectiveUtilitiesRuntime",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
PrivateDependencyModuleNames.Add("UnrealEd");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "DirectiveUtilitiesBlueprintNodes.h"
|
||||||
|
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Nodes/DirectiveUtilMapNodeMigration.h"
|
||||||
|
#include "UObject/UObjectGlobals.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
|
||||||
|
void FDirectiveUtilitiesBlueprintNodesModule::StartupModule()
|
||||||
|
{
|
||||||
|
AssetLoadedHandle = FCoreUObjectDelegates::OnAssetLoaded.AddRaw(this, &FDirectiveUtilitiesBlueprintNodesModule::HandleAssetLoaded);
|
||||||
|
|
||||||
|
for (TObjectIterator<UBlueprint> Blueprint; Blueprint; ++Blueprint)
|
||||||
|
{
|
||||||
|
HandleAssetLoaded(*Blueprint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FDirectiveUtilitiesBlueprintNodesModule::ShutdownModule()
|
||||||
|
{
|
||||||
|
FCoreUObjectDelegates::OnAssetLoaded.Remove(AssetLoadedHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FDirectiveUtilitiesBlueprintNodesModule::HandleAssetLoaded(UObject* Asset)
|
||||||
|
{
|
||||||
|
UBlueprint* Blueprint = Cast<UBlueprint>(Asset);
|
||||||
|
if (Blueprint && !Blueprint->HasAnyFlags(RF_Transient))
|
||||||
|
{
|
||||||
|
DirectiveUtilMapNodeMigration::UpgradeLegacyAppendNodes(*Blueprint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPLEMENT_MODULE(FDirectiveUtilitiesBlueprintNodesModule, DirectiveUtilitiesBlueprintNodes)
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "Nodes/DirectiveUtilMapNodeMigration.h"
|
||||||
|
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "EdGraphSchema_K2.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "K2Node_CallFunction.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "Libraries/DirectiveUtilMapFunctionLibrary.h"
|
||||||
|
#include "Nodes/K2Node_DirectiveUtilMapAppend.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
bool IsLegacyAppendNode(const UK2Node_CallFunction& Node)
|
||||||
|
{
|
||||||
|
return !Node.IsA<UK2Node_DirectiveUtilMapAppend>()
|
||||||
|
&& Node.FunctionReference.GetMemberParentClass() == UDirectiveUtilMapFunctionLibrary::StaticClass()
|
||||||
|
&& Node.FunctionReference.GetMemberName() == GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMapFunctionLibrary, Map_Append);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CopyMapPinTypes(const UK2Node_CallFunction& LegacyNode, UK2Node_DirectiveUtilMapAppend& NewNode)
|
||||||
|
{
|
||||||
|
const FEdGraphPinType* MapType = nullptr;
|
||||||
|
for (const FName PinName : {FName(TEXT("TargetMap")), FName(TEXT("SourceMap"))})
|
||||||
|
{
|
||||||
|
const UEdGraphPin* LegacyPin = LegacyNode.FindPin(PinName);
|
||||||
|
if (!LegacyPin)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!LegacyPin->LinkedTo.IsEmpty() && LegacyPin->LinkedTo[0]->PinType.IsMap())
|
||||||
|
{
|
||||||
|
MapType = &LegacyPin->LinkedTo[0]->PinType;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (LegacyPin->PinType.IsMap()
|
||||||
|
&& LegacyPin->PinType.PinCategory != UEdGraphSchema_K2::PC_Wildcard
|
||||||
|
&& LegacyPin->PinType.PinValueType.TerminalCategory != UEdGraphSchema_K2::PC_Wildcard)
|
||||||
|
{
|
||||||
|
MapType = &LegacyPin->PinType;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MapType)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const FName PinName : {FName(TEXT("TargetMap")), FName(TEXT("SourceMap"))})
|
||||||
|
{
|
||||||
|
if (UEdGraphPin* NewPin = NewNode.FindPin(PinName))
|
||||||
|
{
|
||||||
|
NewPin->PinType.PinCategory = MapType->PinCategory;
|
||||||
|
NewPin->PinType.PinSubCategory = MapType->PinSubCategory;
|
||||||
|
NewPin->PinType.PinSubCategoryObject = MapType->PinSubCategoryObject;
|
||||||
|
NewPin->PinType.PinSubCategoryMemberReference = MapType->PinSubCategoryMemberReference;
|
||||||
|
NewPin->PinType.PinValueType = MapType->PinValueType;
|
||||||
|
NewPin->PinType.bIsWeakPointer = MapType->bIsWeakPointer;
|
||||||
|
NewPin->PinType.bIsUObjectWrapper = MapType->bIsUObjectWrapper;
|
||||||
|
NewPin->PinType.bSerializeAsSinglePrecisionFloat = MapType->bSerializeAsSinglePrecisionFloat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReplaceLegacyAppendNode(UK2Node_CallFunction& LegacyNode)
|
||||||
|
{
|
||||||
|
UEdGraph* Graph = LegacyNode.GetGraph();
|
||||||
|
const UEdGraphSchema_K2* Schema = Graph ? Cast<UEdGraphSchema_K2>(Graph->GetSchema()) : nullptr;
|
||||||
|
if (!Graph || !Schema)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UK2Node_DirectiveUtilMapAppend* NewNode = NewObject<UK2Node_DirectiveUtilMapAppend>(Graph, NAME_None, RF_Transactional);
|
||||||
|
Graph->AddNode(NewNode, false, false);
|
||||||
|
NewNode->CreateNewGuid();
|
||||||
|
NewNode->PostPlacedNewNode();
|
||||||
|
NewNode->AllocateDefaultPins();
|
||||||
|
NewNode->AdvancedPinDisplay = LegacyNode.AdvancedPinDisplay;
|
||||||
|
NewNode->SetEnabledState(LegacyNode.GetDesiredEnabledState(), LegacyNode.HasUserSetTheEnabledState());
|
||||||
|
CopyMapPinTypes(LegacyNode, *NewNode);
|
||||||
|
|
||||||
|
if (Schema->ReplaceOldNodeWithNew(&LegacyNode, NewNode, {}))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
NewNode->DestroyNode();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DirectiveUtilMapNodeMigration::UpgradeLegacyAppendNodes(UBlueprint& Blueprint)
|
||||||
|
{
|
||||||
|
TArray<UEdGraph*> Graphs;
|
||||||
|
Blueprint.GetAllGraphs(Graphs);
|
||||||
|
|
||||||
|
bool bModified = false;
|
||||||
|
for (UEdGraph* Graph : Graphs)
|
||||||
|
{
|
||||||
|
if (!Graph)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<UK2Node_CallFunction*> FunctionNodes;
|
||||||
|
Graph->GetNodesOfClass(FunctionNodes);
|
||||||
|
for (UK2Node_CallFunction* FunctionNode : FunctionNodes)
|
||||||
|
{
|
||||||
|
if (FunctionNode && IsLegacyAppendNode(*FunctionNode))
|
||||||
|
{
|
||||||
|
bModified |= ReplaceLegacyAppendNode(*FunctionNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bModified)
|
||||||
|
{
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsModified(&Blueprint);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bModified;
|
||||||
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "Nodes/K2Node_DirectiveUtilMapAppend.h"
|
||||||
|
|
||||||
|
#include "BlueprintActionDatabaseRegistrar.h"
|
||||||
|
#include "BlueprintNodeSpawner.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "EdGraphSchema_K2.h"
|
||||||
|
#include "Kismet2/CompilerResultsLog.h"
|
||||||
|
#include "Libraries/DirectiveUtilMapFunctionLibrary.h"
|
||||||
|
|
||||||
|
#define LOCTEXT_NAMESPACE "K2Node_DirectiveUtilMapAppend"
|
||||||
|
|
||||||
|
namespace DirectiveUtilMapAppendPins
|
||||||
|
{
|
||||||
|
const FName TargetMap(TEXT("TargetMap"));
|
||||||
|
const FName SourceMap(TEXT("SourceMap"));
|
||||||
|
}
|
||||||
|
|
||||||
|
UK2Node_DirectiveUtilMapAppend::UK2Node_DirectiveUtilMapAppend()
|
||||||
|
{
|
||||||
|
FunctionReference.SetExternalMember(
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMapFunctionLibrary, Map_Append),
|
||||||
|
UDirectiveUtilMapFunctionLibrary::StaticClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
void UK2Node_DirectiveUtilMapAppend::AllocateDefaultPins()
|
||||||
|
{
|
||||||
|
Super::AllocateDefaultPins();
|
||||||
|
ConformMapPins();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UK2Node_DirectiveUtilMapAppend::PostReconstructNode()
|
||||||
|
{
|
||||||
|
Super::PostReconstructNode();
|
||||||
|
ConformMapPins();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UK2Node_DirectiveUtilMapAppend::NotifyPinConnectionListChanged(UEdGraphPin* Pin)
|
||||||
|
{
|
||||||
|
Super::NotifyPinConnectionListChanged(Pin);
|
||||||
|
|
||||||
|
if (IsAppendMapPin(Pin) && ConformMapPins())
|
||||||
|
{
|
||||||
|
GetGraph()->NotifyGraphChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UK2Node_DirectiveUtilMapAppend::ValidateNodeDuringCompilation(FCompilerResultsLog& MessageLog) const
|
||||||
|
{
|
||||||
|
Super::ValidateNodeDuringCompilation(MessageLog);
|
||||||
|
|
||||||
|
const UEdGraphPin* TargetMap = FindPin(DirectiveUtilMapAppendPins::TargetMap);
|
||||||
|
const UEdGraphPin* SourceMap = FindPin(DirectiveUtilMapAppendPins::SourceMap);
|
||||||
|
if (!TargetMap || !SourceMap || TargetMap->LinkedTo.IsEmpty() || SourceMap->LinkedTo.IsEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!HaveMatchingMapTypes(TargetMap->LinkedTo[0]->PinType, SourceMap->LinkedTo[0]->PinType))
|
||||||
|
{
|
||||||
|
MessageLog.Error(*LOCTEXT("MismatchedMapTypes", "Append requires matching map types on @@.").ToString(), this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UK2Node_DirectiveUtilMapAppend::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const
|
||||||
|
{
|
||||||
|
UClass* ActionKey = GetClass();
|
||||||
|
if (ActionRegistrar.IsOpenForRegistration(ActionKey))
|
||||||
|
{
|
||||||
|
UBlueprintNodeSpawner* NodeSpawner = UBlueprintNodeSpawner::Create(GetClass());
|
||||||
|
check(NodeSpawner);
|
||||||
|
ActionRegistrar.AddBlueprintAction(ActionKey, NodeSpawner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UK2Node_DirectiveUtilMapAppend::IsConnectionDisallowed(
|
||||||
|
const UEdGraphPin* MyPin,
|
||||||
|
const UEdGraphPin* OtherPin,
|
||||||
|
FString& OutReason) const
|
||||||
|
{
|
||||||
|
if (Super::IsConnectionDisallowed(MyPin, OtherPin, OutReason))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsAppendMapPin(MyPin) || !OtherPin || !IsConcreteMap(OtherPin->PinType))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FName SiblingName = MyPin->PinName == DirectiveUtilMapAppendPins::TargetMap
|
||||||
|
? DirectiveUtilMapAppendPins::SourceMap
|
||||||
|
: DirectiveUtilMapAppendPins::TargetMap;
|
||||||
|
const UEdGraphPin* SiblingPin = FindPin(SiblingName);
|
||||||
|
if (!SiblingPin)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FEdGraphPinType* SiblingType = &SiblingPin->PinType;
|
||||||
|
if (!SiblingPin->LinkedTo.IsEmpty())
|
||||||
|
{
|
||||||
|
SiblingType = &SiblingPin->LinkedTo[0]->PinType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsConcreteMap(*SiblingType) && !HaveMatchingMapTypes(*SiblingType, OtherPin->PinType))
|
||||||
|
{
|
||||||
|
OutReason = LOCTEXT("MapTypeMismatch", "Both maps must have the same key and value types.").ToString();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UK2Node_DirectiveUtilMapAppend::ConformMapPins()
|
||||||
|
{
|
||||||
|
UEdGraphPin* TargetMap = FindPin(DirectiveUtilMapAppendPins::TargetMap);
|
||||||
|
UEdGraphPin* SourceMap = FindPin(DirectiveUtilMapAppendPins::SourceMap);
|
||||||
|
if (!TargetMap || !SourceMap)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FEdGraphPinType* ResolvedType = nullptr;
|
||||||
|
for (const UEdGraphPin* MapPin : {TargetMap, SourceMap})
|
||||||
|
{
|
||||||
|
if (!MapPin->LinkedTo.IsEmpty() && IsConcreteMap(MapPin->LinkedTo[0]->PinType))
|
||||||
|
{
|
||||||
|
ResolvedType = &MapPin->LinkedTo[0]->PinType;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FEdGraphPinType PreviousTargetType = TargetMap->PinType;
|
||||||
|
const FEdGraphPinType PreviousSourceType = SourceMap->PinType;
|
||||||
|
if (ResolvedType)
|
||||||
|
{
|
||||||
|
ApplyMapType(*TargetMap, *ResolvedType);
|
||||||
|
ApplyMapType(*SourceMap, *ResolvedType);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ResetMapType(*TargetMap);
|
||||||
|
ResetMapType(*SourceMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
return PreviousTargetType != TargetMap->PinType || PreviousSourceType != SourceMap->PinType;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UK2Node_DirectiveUtilMapAppend::IsAppendMapPin(const UEdGraphPin* Pin)
|
||||||
|
{
|
||||||
|
return Pin && (Pin->PinName == DirectiveUtilMapAppendPins::TargetMap || Pin->PinName == DirectiveUtilMapAppendPins::SourceMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UK2Node_DirectiveUtilMapAppend::IsConcreteMap(const FEdGraphPinType& PinType)
|
||||||
|
{
|
||||||
|
return PinType.IsMap()
|
||||||
|
&& PinType.PinCategory != UEdGraphSchema_K2::PC_Wildcard
|
||||||
|
&& PinType.PinValueType.TerminalCategory != UEdGraphSchema_K2::PC_Wildcard;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UK2Node_DirectiveUtilMapAppend::HaveMatchingMapTypes(
|
||||||
|
const FEdGraphPinType& First,
|
||||||
|
const FEdGraphPinType& Second)
|
||||||
|
{
|
||||||
|
return First.IsMap()
|
||||||
|
&& Second.IsMap()
|
||||||
|
&& First.PinCategory == Second.PinCategory
|
||||||
|
&& First.PinSubCategory == Second.PinSubCategory
|
||||||
|
&& First.PinSubCategoryObject == Second.PinSubCategoryObject
|
||||||
|
&& First.bIsWeakPointer == Second.bIsWeakPointer
|
||||||
|
&& First.bIsUObjectWrapper == Second.bIsUObjectWrapper
|
||||||
|
&& First.PinValueType.TerminalCategory == Second.PinValueType.TerminalCategory
|
||||||
|
&& First.PinValueType.TerminalSubCategory == Second.PinValueType.TerminalSubCategory
|
||||||
|
&& First.PinValueType.TerminalSubCategoryObject == Second.PinValueType.TerminalSubCategoryObject
|
||||||
|
&& First.PinValueType.bTerminalIsWeakPointer == Second.PinValueType.bTerminalIsWeakPointer
|
||||||
|
&& First.PinValueType.bTerminalIsUObjectWrapper == Second.PinValueType.bTerminalIsUObjectWrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UK2Node_DirectiveUtilMapAppend::ApplyMapType(UEdGraphPin& Pin, const FEdGraphPinType& MapType)
|
||||||
|
{
|
||||||
|
Pin.PinType.PinCategory = MapType.PinCategory;
|
||||||
|
Pin.PinType.PinSubCategory = MapType.PinSubCategory;
|
||||||
|
Pin.PinType.PinSubCategoryObject = MapType.PinSubCategoryObject;
|
||||||
|
Pin.PinType.PinSubCategoryMemberReference = MapType.PinSubCategoryMemberReference;
|
||||||
|
Pin.PinType.PinValueType = MapType.PinValueType;
|
||||||
|
Pin.PinType.bIsWeakPointer = MapType.bIsWeakPointer;
|
||||||
|
Pin.PinType.bIsUObjectWrapper = MapType.bIsUObjectWrapper;
|
||||||
|
Pin.PinType.bSerializeAsSinglePrecisionFloat = MapType.bSerializeAsSinglePrecisionFloat;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UK2Node_DirectiveUtilMapAppend::ResetMapType(UEdGraphPin& Pin)
|
||||||
|
{
|
||||||
|
Pin.PinType.PinCategory = UEdGraphSchema_K2::PC_Wildcard;
|
||||||
|
Pin.PinType.PinSubCategory = NAME_None;
|
||||||
|
Pin.PinType.PinSubCategoryObject = nullptr;
|
||||||
|
Pin.PinType.PinSubCategoryMemberReference = FSimpleMemberReference();
|
||||||
|
Pin.PinType.PinValueType = FEdGraphTerminalType();
|
||||||
|
Pin.PinType.PinValueType.TerminalCategory = UEdGraphSchema_K2::PC_Wildcard;
|
||||||
|
Pin.PinType.bIsWeakPointer = false;
|
||||||
|
Pin.PinType.bIsUObjectWrapper = false;
|
||||||
|
Pin.PinType.bSerializeAsSinglePrecisionFloat = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef LOCTEXT_NAMESPACE
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Modules/ModuleManager.h"
|
||||||
|
|
||||||
|
class UObject;
|
||||||
|
|
||||||
|
class FDirectiveUtilitiesBlueprintNodesModule : public IModuleInterface
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual void StartupModule() override;
|
||||||
|
virtual void ShutdownModule() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void HandleAssetLoaded(UObject* Asset);
|
||||||
|
|
||||||
|
FDelegateHandle AssetLoadedHandle;
|
||||||
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreTypes.h"
|
||||||
|
|
||||||
|
class UBlueprint;
|
||||||
|
|
||||||
|
namespace DirectiveUtilMapNodeMigration
|
||||||
|
{
|
||||||
|
DIRECTIVEUTILITIESBLUEPRINTNODES_API bool UpgradeLegacyAppendNodes(UBlueprint& Blueprint);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "K2Node_CallFunction.h"
|
||||||
|
#include "K2Node_DirectiveUtilMapAppend.generated.h"
|
||||||
|
|
||||||
|
class FBlueprintActionDatabaseRegistrar;
|
||||||
|
class FCompilerResultsLog;
|
||||||
|
class UEdGraphPin;
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class DIRECTIVEUTILITIESBLUEPRINTNODES_API UK2Node_DirectiveUtilMapAppend : public UK2Node_CallFunction
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UK2Node_DirectiveUtilMapAppend();
|
||||||
|
|
||||||
|
virtual void AllocateDefaultPins() override;
|
||||||
|
virtual void PostReconstructNode() override;
|
||||||
|
virtual void NotifyPinConnectionListChanged(UEdGraphPin* Pin) override;
|
||||||
|
virtual void ValidateNodeDuringCompilation(FCompilerResultsLog& MessageLog) const override;
|
||||||
|
virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override;
|
||||||
|
virtual bool IsConnectionDisallowed(const UEdGraphPin* MyPin, const UEdGraphPin* OtherPin, FString& OutReason) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool ConformMapPins();
|
||||||
|
static bool IsAppendMapPin(const UEdGraphPin* Pin);
|
||||||
|
static bool IsConcreteMap(const FEdGraphPinType& PinType);
|
||||||
|
static bool HaveMatchingMapTypes(const FEdGraphPinType& First, const FEdGraphPinType& Second);
|
||||||
|
static void ApplyMapType(UEdGraphPin& Pin, const FEdGraphPinType& MapType);
|
||||||
|
static void ResetMapType(UEdGraphPin& Pin);
|
||||||
|
};
|
||||||
@@ -16,6 +16,7 @@ public class DirectiveUtilitiesEditor : ModuleRules
|
|||||||
"Engine",
|
"Engine",
|
||||||
"EditorSubsystem",
|
"EditorSubsystem",
|
||||||
"EditorScriptingUtilities",
|
"EditorScriptingUtilities",
|
||||||
|
"AssetRegistry",
|
||||||
"DirectiveUtilitiesRuntime",
|
"DirectiveUtilitiesRuntime",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -26,7 +27,6 @@ public class DirectiveUtilitiesEditor : ModuleRules
|
|||||||
"Slate",
|
"Slate",
|
||||||
"SlateCore",
|
"SlateCore",
|
||||||
"UnrealEd",
|
"UnrealEd",
|
||||||
"AssetRegistry",
|
|
||||||
"AssetTools",
|
"AssetTools",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
|
||||||
|
namespace DirectiveUtilitiesEditor
|
||||||
|
{
|
||||||
|
inline void EnsureAssetRegistryScan(IAssetRegistry& AssetRegistry)
|
||||||
|
{
|
||||||
|
if (!AssetRegistry.IsSearchAllAssets())
|
||||||
|
{
|
||||||
|
AssetRegistry.SearchAllAssets(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AssetRegistry.IsLoadingAssets())
|
||||||
|
{
|
||||||
|
AssetRegistry.WaitForCompletion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "AssetRegistry/DirectiveUtilDependencyCycleFinder.h"
|
||||||
|
|
||||||
|
namespace DirectiveUtilitiesEditor
|
||||||
|
{
|
||||||
|
FDependencyCycleFinder::FDependencyCycleFinder(const TMap<FName, TArray<FName>>& InGraph)
|
||||||
|
: Graph(InGraph)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FDirectiveUtilAssetDependencyCycle> FDependencyCycleFinder::Find() const
|
||||||
|
{
|
||||||
|
const TArray<FName> FinishOrder = BuildFinishOrder();
|
||||||
|
const TMap<FName, TArray<FName>> ReverseGraph = BuildReverseGraph();
|
||||||
|
TSet<FName> Visited;
|
||||||
|
TArray<FDirectiveUtilAssetDependencyCycle> Cycles;
|
||||||
|
for (int32 Index = FinishOrder.Num() - 1; Index >= 0; --Index)
|
||||||
|
{
|
||||||
|
const FName Root = FinishOrder[Index];
|
||||||
|
if (Visited.Contains(Root))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
FDirectiveUtilAssetDependencyCycle Cycle;
|
||||||
|
TArray<FName> Pending = {Root};
|
||||||
|
Visited.Add(Root);
|
||||||
|
while (!Pending.IsEmpty())
|
||||||
|
{
|
||||||
|
const FName Package = Pending.Pop(EAllowShrinking::No);
|
||||||
|
Cycle.Packages.Add(Package);
|
||||||
|
const TArray<FName>* Referencers = ReverseGraph.Find(Package);
|
||||||
|
if (!Referencers)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const FName Referencer : *Referencers)
|
||||||
|
{
|
||||||
|
if (!Visited.Contains(Referencer))
|
||||||
|
{
|
||||||
|
Visited.Add(Referencer);
|
||||||
|
Pending.Add(Referencer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TArray<FName>* RootDependencies = Cycle.Packages.Num() == 1
|
||||||
|
? Graph.Find(Cycle.Packages[0])
|
||||||
|
: nullptr;
|
||||||
|
const bool bSelfCycle = RootDependencies && RootDependencies->Contains(Cycle.Packages[0]);
|
||||||
|
if (Cycle.Packages.Num() > 1 || bSelfCycle)
|
||||||
|
{
|
||||||
|
Cycle.Packages.Sort(FNameLexicalLess());
|
||||||
|
Cycles.Add(MoveTemp(Cycle));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cycles.Sort([](const FDirectiveUtilAssetDependencyCycle& Left, const FDirectiveUtilAssetDependencyCycle& Right) {
|
||||||
|
return Left.Packages[0].LexicalLess(Right.Packages[0]);
|
||||||
|
});
|
||||||
|
return Cycles;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FName> FDependencyCycleFinder::BuildFinishOrder() const
|
||||||
|
{
|
||||||
|
TArray<FName> Packages;
|
||||||
|
Graph.GetKeys(Packages);
|
||||||
|
Packages.Sort(FNameLexicalLess());
|
||||||
|
|
||||||
|
TArray<FName> FinishOrder;
|
||||||
|
FinishOrder.Reserve(Packages.Num());
|
||||||
|
TSet<FName> Visited;
|
||||||
|
for (const FName Root : Packages)
|
||||||
|
{
|
||||||
|
if (Visited.Contains(Root))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FTraversalFrame> Pending = {{Root, 0}};
|
||||||
|
Visited.Add(Root);
|
||||||
|
while (!Pending.IsEmpty())
|
||||||
|
{
|
||||||
|
FTraversalFrame& Frame = Pending.Last();
|
||||||
|
const TArray<FName>* Dependencies = Graph.Find(Frame.Package);
|
||||||
|
if (Dependencies && Frame.NextDependencyIndex < Dependencies->Num())
|
||||||
|
{
|
||||||
|
const FName Dependency = (*Dependencies)[Frame.NextDependencyIndex++];
|
||||||
|
if (!Visited.Contains(Dependency))
|
||||||
|
{
|
||||||
|
Visited.Add(Dependency);
|
||||||
|
Pending.Add({Dependency, 0});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
FinishOrder.Add(Frame.Package);
|
||||||
|
Pending.Pop(EAllowShrinking::No);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FinishOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
TMap<FName, TArray<FName>> FDependencyCycleFinder::BuildReverseGraph() const
|
||||||
|
{
|
||||||
|
TMap<FName, TArray<FName>> ReverseGraph;
|
||||||
|
for (const TPair<FName, TArray<FName>>& Node : Graph)
|
||||||
|
{
|
||||||
|
ReverseGraph.FindOrAdd(Node.Key);
|
||||||
|
for (const FName Dependency : Node.Value)
|
||||||
|
{
|
||||||
|
ReverseGraph.FindOrAdd(Dependency).Add(Node.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (TPair<FName, TArray<FName>>& Node : ReverseGraph)
|
||||||
|
{
|
||||||
|
Node.Value.Sort(FNameLexicalLess());
|
||||||
|
}
|
||||||
|
return ReverseGraph;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Types/DirectiveUtilEditorAuditTypes.h"
|
||||||
|
|
||||||
|
namespace DirectiveUtilitiesEditor
|
||||||
|
{
|
||||||
|
class DIRECTIVEUTILITIESEDITOR_API FDependencyCycleFinder
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit FDependencyCycleFinder(const TMap<FName, TArray<FName>>& InGraph);
|
||||||
|
|
||||||
|
TArray<FDirectiveUtilAssetDependencyCycle> Find() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct FTraversalFrame
|
||||||
|
{
|
||||||
|
FName Package;
|
||||||
|
int32 NextDependencyIndex = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
TArray<FName> BuildFinishOrder() const;
|
||||||
|
TMap<FName, TArray<FName>> BuildReverseGraph() const;
|
||||||
|
|
||||||
|
const TMap<FName, TArray<FName>>& Graph;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
#include "DirectiveUtilitiesEditor.h"
|
#include "DirectiveUtilitiesEditor.h"
|
||||||
|
#include "Tasks/DirectiveUtilEditorSlowTask.h"
|
||||||
|
|
||||||
void FDirectiveUtilitiesEditorModule::StartupModule()
|
void FDirectiveUtilitiesEditorModule::StartupModule()
|
||||||
{
|
{
|
||||||
@@ -8,6 +9,7 @@ void FDirectiveUtilitiesEditorModule::StartupModule()
|
|||||||
|
|
||||||
void FDirectiveUtilitiesEditorModule::ShutdownModule()
|
void FDirectiveUtilitiesEditorModule::ShutdownModule()
|
||||||
{
|
{
|
||||||
|
UDirectiveUtilEditorSlowTask::FinishActiveTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
IMPLEMENT_MODULE(FDirectiveUtilitiesEditorModule, DirectiveUtilitiesEditor)
|
IMPLEMENT_MODULE(FDirectiveUtilitiesEditorModule, DirectiveUtilitiesEditor)
|
||||||
|
|||||||
@@ -0,0 +1,433 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "Libraries/DirectiveUtilEditorAssetAuditLibrary.h"
|
||||||
|
|
||||||
|
#include "AssetRegistry/DirectiveUtilAssetRegistry.h"
|
||||||
|
#include "AssetRegistry/DirectiveUtilDependencyCycleFinder.h"
|
||||||
|
#include "AssetRegistry/ARFilter.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "Engine/AssetManager.h"
|
||||||
|
#include "HAL/FileManager.h"
|
||||||
|
#include "Misc/PackageName.h"
|
||||||
|
#include "UObject/ObjectRedirector.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
using namespace UE::AssetRegistry;
|
||||||
|
|
||||||
|
bool IsPackageInsideAuditPath(const FName PackageName, const FName Path)
|
||||||
|
{
|
||||||
|
const FString Package = PackageName.ToString();
|
||||||
|
FString Parent = Path.ToString();
|
||||||
|
Parent.RemoveFromEnd(TEXT("/"));
|
||||||
|
return Package == Parent || Package.StartsWith(Parent + TEXT("/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ContainsPathSegment(const FName PackagePath, const FString& Segment)
|
||||||
|
{
|
||||||
|
const FString Path = TEXT("/") + PackagePath.ToString().TrimChar(TEXT('/')) + TEXT("/");
|
||||||
|
return Path.Contains(TEXT("/") + Segment + TEXT("/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsExcluded(const FAssetData& Asset, const FDirectiveUtilAssetAuditOptions& Options)
|
||||||
|
{
|
||||||
|
if (Asset.AssetClassPath == UObjectRedirector::StaticClass()->GetClassPathName())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Options.bUseDefaultPathExclusions
|
||||||
|
&& (ContainsPathSegment(Asset.PackagePath, TEXT("Developers"))
|
||||||
|
|| ContainsPathSegment(Asset.PackagePath, TEXT("__ExternalActors__"))
|
||||||
|
|| ContainsPathSegment(Asset.PackagePath, TEXT("__ExternalObjects__"))
|
||||||
|
|| ContainsPathSegment(Asset.PackagePath, TEXT("Test"))
|
||||||
|
|| ContainsPathSegment(Asset.PackagePath, TEXT("Tests"))))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const FName Path : Options.ExcludedPackagePaths)
|
||||||
|
{
|
||||||
|
if (IsPackageInsideAuditPath(Asset.PackageName, Path))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FAssetData> GetScannedAssets(IAssetRegistry& AssetRegistry, const FDirectiveUtilAssetAuditOptions& Options)
|
||||||
|
{
|
||||||
|
DirectiveUtilitiesEditor::EnsureAssetRegistryScan(AssetRegistry);
|
||||||
|
|
||||||
|
FARFilter Filter;
|
||||||
|
Filter.bRecursivePaths = true;
|
||||||
|
if (Options.PackagePaths.IsEmpty())
|
||||||
|
{
|
||||||
|
Filter.PackagePaths.Add(TEXT("/Game"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Filter.PackagePaths.Append(Options.PackagePaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FAssetData> Assets;
|
||||||
|
AssetRegistry.GetAssets(Filter, Assets);
|
||||||
|
Assets.RemoveAll([&Options](const FAssetData& Asset) {
|
||||||
|
return !Asset.IsValid() || IsExcluded(Asset, Options);
|
||||||
|
});
|
||||||
|
Assets.Sort([](const FAssetData& Left, const FAssetData& Right) {
|
||||||
|
return Left.GetSoftObjectPath().LexicalLess(Right.GetSoftObjectPath());
|
||||||
|
});
|
||||||
|
return Assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsPrimaryAsset(const FAssetData& Asset)
|
||||||
|
{
|
||||||
|
const UAssetManager* AssetManager = UAssetManager::GetIfInitialized();
|
||||||
|
return AssetManager && AssetManager->GetPrimaryAssetIdForData(Asset).IsValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
EDirectiveUtilAssetDependencyType GetDependencyType(const FAssetDependency& Dependency)
|
||||||
|
{
|
||||||
|
if (EnumHasAnyFlags(Dependency.Category, EDependencyCategory::SearchableName))
|
||||||
|
{
|
||||||
|
return EDirectiveUtilAssetDependencyType::SearchableName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EnumHasAnyFlags(Dependency.Category, EDependencyCategory::Manage))
|
||||||
|
{
|
||||||
|
return EnumHasAnyFlags(Dependency.Properties, EDependencyProperty::Direct)
|
||||||
|
? EDirectiveUtilAssetDependencyType::DirectManagement
|
||||||
|
: EDirectiveUtilAssetDependencyType::IndirectManagement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EnumHasAnyFlags(Dependency.Properties, EDependencyProperty::Hard)
|
||||||
|
? EDirectiveUtilAssetDependencyType::HardPackage
|
||||||
|
: EDirectiveUtilAssetDependencyType::SoftPackage;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PackageExists(IAssetRegistry& AssetRegistry, const FName PackageName)
|
||||||
|
{
|
||||||
|
if (PackageName.IsNone())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FString Package = PackageName.ToString();
|
||||||
|
if (Package.StartsWith(TEXT("/Script/")) || Package.StartsWith(TEXT("/Memory/")) || Package.StartsWith(TEXT("/Temp/")))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (FindPackage(nullptr, *Package))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FAssetData> PackageAssets;
|
||||||
|
return AssetRegistry.GetAssetsByPackageName(PackageName, PackageAssets, true) && !PackageAssets.IsEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
class FAssetAuditScan
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FAssetAuditScan(IAssetRegistry& InAssetRegistry, const FDirectiveUtilAssetAuditOptions& InOptions)
|
||||||
|
: AssetRegistry(InAssetRegistry)
|
||||||
|
, bIncludePrimaryAssets(InOptions.bIncludePrimaryAssets)
|
||||||
|
, Assets(GetScannedAssets(InAssetRegistry, InOptions))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
const TArray<FAssetData>& GetAssets() const
|
||||||
|
{
|
||||||
|
return Assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TArray<FAssetDependency>& GetDependencies(const FName PackageName)
|
||||||
|
{
|
||||||
|
if (const TArray<FAssetDependency>* Existing = Dependencies.Find(PackageName))
|
||||||
|
{
|
||||||
|
return *Existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FAssetDependency>& Result = Dependencies.Add(PackageName);
|
||||||
|
AssetRegistry.GetDependencies(FAssetIdentifier(PackageName), Result, EDependencyCategory::All);
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TArray<FName>& GetReferencers(const FName PackageName)
|
||||||
|
{
|
||||||
|
if (const TArray<FName>* Existing = Referencers.Find(PackageName))
|
||||||
|
{
|
||||||
|
return *Existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FName>& Result = Referencers.Add(PackageName);
|
||||||
|
AssetRegistry.GetReferencers(PackageName, Result, EDependencyCategory::All);
|
||||||
|
Result.Remove(PackageName);
|
||||||
|
Result.Sort(FNameLexicalLess());
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IncludesPrimaryAssets() const
|
||||||
|
{
|
||||||
|
return bIncludePrimaryAssets;
|
||||||
|
}
|
||||||
|
|
||||||
|
IAssetRegistry& GetAssetRegistry() const
|
||||||
|
{
|
||||||
|
return AssetRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
IAssetRegistry& AssetRegistry;
|
||||||
|
bool bIncludePrimaryAssets;
|
||||||
|
TArray<FAssetData> Assets;
|
||||||
|
TMap<FName, TArray<FAssetDependency>> Dependencies;
|
||||||
|
TMap<FName, TArray<FName>> Referencers;
|
||||||
|
};
|
||||||
|
|
||||||
|
FString EscapeCsv(const FString& Value)
|
||||||
|
{
|
||||||
|
FString Escaped = Value.Replace(TEXT("\""), TEXT("\"\""));
|
||||||
|
return FString::Printf(TEXT("\"%s\""), *Escaped);
|
||||||
|
}
|
||||||
|
|
||||||
|
FString DependencyTypeToString(const EDirectiveUtilAssetDependencyType Type)
|
||||||
|
{
|
||||||
|
switch (Type)
|
||||||
|
{
|
||||||
|
case EDirectiveUtilAssetDependencyType::HardPackage:
|
||||||
|
return TEXT("Hard Package");
|
||||||
|
case EDirectiveUtilAssetDependencyType::SoftPackage:
|
||||||
|
return TEXT("Soft Package");
|
||||||
|
case EDirectiveUtilAssetDependencyType::SearchableName:
|
||||||
|
return TEXT("Searchable Name");
|
||||||
|
case EDirectiveUtilAssetDependencyType::DirectManagement:
|
||||||
|
return TEXT("Direct Management");
|
||||||
|
case EDirectiveUtilAssetDependencyType::IndirectManagement:
|
||||||
|
return TEXT("Indirect Management");
|
||||||
|
}
|
||||||
|
|
||||||
|
return TEXT("Unknown");
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FAssetData> FindUnreferencedCandidates(FAssetAuditScan& Scan)
|
||||||
|
{
|
||||||
|
TArray<FAssetData> Candidates;
|
||||||
|
for (const FAssetData& Asset : Scan.GetAssets())
|
||||||
|
{
|
||||||
|
if ((!IsPrimaryAsset(Asset) || Scan.IncludesPrimaryAssets()) && Scan.GetReferencers(Asset.PackageName).IsEmpty())
|
||||||
|
{
|
||||||
|
Candidates.Add(Asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FDirectiveUtilMissingAssetReference> FindMissingReferences(FAssetAuditScan& Scan)
|
||||||
|
{
|
||||||
|
TArray<FDirectiveUtilMissingAssetReference> MissingReferences;
|
||||||
|
for (const FAssetData& Asset : Scan.GetAssets())
|
||||||
|
{
|
||||||
|
for (const FAssetDependency& Dependency : Scan.GetDependencies(Asset.PackageName))
|
||||||
|
{
|
||||||
|
if (!PackageExists(Scan.GetAssetRegistry(), Dependency.AssetId.PackageName))
|
||||||
|
{
|
||||||
|
FDirectiveUtilMissingAssetReference& MissingReference = MissingReferences.AddDefaulted_GetRef();
|
||||||
|
MissingReference.ReferencingAsset = Asset;
|
||||||
|
MissingReference.MissingPackage = Dependency.AssetId.PackageName;
|
||||||
|
MissingReference.DependencyType = GetDependencyType(Dependency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MissingReferences.Sort([](const FDirectiveUtilMissingAssetReference& Left, const FDirectiveUtilMissingAssetReference& Right) {
|
||||||
|
if (Left.ReferencingAsset.PackageName != Right.ReferencingAsset.PackageName)
|
||||||
|
{
|
||||||
|
return Left.ReferencingAsset.PackageName.LexicalLess(Right.ReferencingAsset.PackageName);
|
||||||
|
}
|
||||||
|
return Left.MissingPackage.LexicalLess(Right.MissingPackage);
|
||||||
|
});
|
||||||
|
return MissingReferences;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FDirectiveUtilAssetDependencyCycle> FindDependencyCycles(FAssetAuditScan& Scan)
|
||||||
|
{
|
||||||
|
TSet<FName> ScannedPackages;
|
||||||
|
for (const FAssetData& Asset : Scan.GetAssets())
|
||||||
|
{
|
||||||
|
ScannedPackages.Add(Asset.PackageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
TMap<FName, TArray<FName>> Graph;
|
||||||
|
for (const FName Package : ScannedPackages)
|
||||||
|
{
|
||||||
|
TArray<FName>& Edges = Graph.Add(Package);
|
||||||
|
for (const FAssetDependency& Dependency : Scan.GetDependencies(Package))
|
||||||
|
{
|
||||||
|
if (ScannedPackages.Contains(Dependency.AssetId.PackageName))
|
||||||
|
{
|
||||||
|
Edges.AddUnique(Dependency.AssetId.PackageName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Edges.Sort(FNameLexicalLess());
|
||||||
|
}
|
||||||
|
|
||||||
|
return DirectiveUtilitiesEditor::FDependencyCycleFinder(Graph).Find();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FAssetData> UDirectiveUtilEditorAssetAuditLibrary::FindUnreferencedAssetCandidates(
|
||||||
|
const FDirectiveUtilAssetAuditOptions& Options)
|
||||||
|
{
|
||||||
|
IAssetRegistry* AssetRegistry = IAssetRegistry::Get();
|
||||||
|
if (!AssetRegistry)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
FAssetAuditScan Scan(*AssetRegistry, Options);
|
||||||
|
return FindUnreferencedCandidates(Scan);
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FDirectiveUtilMissingAssetReference> UDirectiveUtilEditorAssetAuditLibrary::FindMissingAssetReferences(
|
||||||
|
const FDirectiveUtilAssetAuditOptions& Options)
|
||||||
|
{
|
||||||
|
IAssetRegistry* AssetRegistry = IAssetRegistry::Get();
|
||||||
|
if (!AssetRegistry)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
FAssetAuditScan Scan(*AssetRegistry, Options);
|
||||||
|
return FindMissingReferences(Scan);
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FDirectiveUtilAssetDependencyCycle> UDirectiveUtilEditorAssetAuditLibrary::FindAssetDependencyCycles(
|
||||||
|
const FDirectiveUtilAssetAuditOptions& Options)
|
||||||
|
{
|
||||||
|
IAssetRegistry* AssetRegistry = IAssetRegistry::Get();
|
||||||
|
if (!AssetRegistry)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
FAssetAuditScan Scan(*AssetRegistry, Options);
|
||||||
|
return FindDependencyCycles(Scan);
|
||||||
|
}
|
||||||
|
|
||||||
|
FDirectiveUtilAssetAuditReport UDirectiveUtilEditorAssetAuditLibrary::BuildAssetAuditReport(
|
||||||
|
const FDirectiveUtilAssetAuditOptions& Options)
|
||||||
|
{
|
||||||
|
FDirectiveUtilAssetAuditReport Report;
|
||||||
|
IAssetRegistry* AssetRegistry = IAssetRegistry::Get();
|
||||||
|
if (!AssetRegistry)
|
||||||
|
{
|
||||||
|
return Report;
|
||||||
|
}
|
||||||
|
|
||||||
|
FAssetAuditScan Scan(*AssetRegistry, Options);
|
||||||
|
const TArray<FAssetData> Candidates = FindUnreferencedCandidates(Scan);
|
||||||
|
Report.MissingReferences = FindMissingReferences(Scan);
|
||||||
|
Report.DependencyCycles = FindDependencyCycles(Scan);
|
||||||
|
|
||||||
|
TSet<FSoftObjectPath> CandidatePaths;
|
||||||
|
for (const FAssetData& Candidate : Candidates)
|
||||||
|
{
|
||||||
|
CandidatePaths.Add(Candidate.GetSoftObjectPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
TMap<FName, int32> MissingCounts;
|
||||||
|
for (const FDirectiveUtilMissingAssetReference& MissingReference : Report.MissingReferences)
|
||||||
|
{
|
||||||
|
++MissingCounts.FindOrAdd(MissingReference.ReferencingAsset.PackageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
TSet<FName> CyclicPackages;
|
||||||
|
for (const FDirectiveUtilAssetDependencyCycle& Cycle : Report.DependencyCycles)
|
||||||
|
{
|
||||||
|
CyclicPackages.Append(Cycle.Packages);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const FAssetData& Asset : Scan.GetAssets())
|
||||||
|
{
|
||||||
|
FDirectiveUtilAssetAuditEntry& Entry = Report.Assets.AddDefaulted_GetRef();
|
||||||
|
Entry.Asset = Asset;
|
||||||
|
Entry.PackageName = Asset.PackageName;
|
||||||
|
Entry.PackagePath = Asset.PackagePath;
|
||||||
|
Entry.AssetClass = Asset.AssetClassPath.ToString();
|
||||||
|
Entry.bPrimaryAsset = IsPrimaryAsset(Asset);
|
||||||
|
Entry.DependencyCount = Scan.GetDependencies(Asset.PackageName).Num();
|
||||||
|
Entry.ReferencerCount = Scan.GetReferencers(Asset.PackageName).Num();
|
||||||
|
|
||||||
|
FString PackageFilename;
|
||||||
|
if (FPackageName::DoesPackageExist(Asset.PackageName.ToString(), &PackageFilename))
|
||||||
|
{
|
||||||
|
Entry.DiskSize = FMath::Max<int64>(IFileManager::Get().FileSize(*PackageFilename), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CandidatePaths.Contains(Asset.GetSoftObjectPath()))
|
||||||
|
{
|
||||||
|
Entry.Findings.Add(TEXT("Unreferenced candidate"));
|
||||||
|
}
|
||||||
|
if (const int32* MissingCount = MissingCounts.Find(Asset.PackageName))
|
||||||
|
{
|
||||||
|
Entry.Findings.Add(FString::Printf(TEXT("%d missing reference%s"), *MissingCount, *MissingCount == 1 ? TEXT("") : TEXT("s")));
|
||||||
|
}
|
||||||
|
if (CyclicPackages.Contains(Asset.PackageName))
|
||||||
|
{
|
||||||
|
Entry.Findings.Add(TEXT("Dependency cycle"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Report;
|
||||||
|
}
|
||||||
|
|
||||||
|
FString UDirectiveUtilEditorAssetAuditLibrary::AssetAuditReportToCsv(const FDirectiveUtilAssetAuditReport& Report)
|
||||||
|
{
|
||||||
|
TArray<FString> Rows;
|
||||||
|
Rows.Add(TEXT("Asset,Package,Path,Class,Disk Size,Dependencies,Referencers,Primary Asset,Findings"));
|
||||||
|
for (const FDirectiveUtilAssetAuditEntry& Entry : Report.Assets)
|
||||||
|
{
|
||||||
|
const TArray<FString> Columns = {
|
||||||
|
EscapeCsv(Entry.Asset.GetSoftObjectPath().ToString()),
|
||||||
|
EscapeCsv(Entry.PackageName.ToString()),
|
||||||
|
EscapeCsv(Entry.PackagePath.ToString()),
|
||||||
|
EscapeCsv(Entry.AssetClass),
|
||||||
|
LexToString(Entry.DiskSize),
|
||||||
|
LexToString(Entry.DependencyCount),
|
||||||
|
LexToString(Entry.ReferencerCount),
|
||||||
|
Entry.bPrimaryAsset ? TEXT("true") : TEXT("false"),
|
||||||
|
EscapeCsv(FString::Join(Entry.Findings, TEXT("; "))),
|
||||||
|
};
|
||||||
|
Rows.Add(FString::Join(Columns, TEXT(",")));
|
||||||
|
}
|
||||||
|
|
||||||
|
Rows.Add(TEXT(""));
|
||||||
|
Rows.Add(TEXT("Missing Reference,Referencing Asset,Dependency Type"));
|
||||||
|
for (const FDirectiveUtilMissingAssetReference& MissingReference : Report.MissingReferences)
|
||||||
|
{
|
||||||
|
const TArray<FString> Columns = {
|
||||||
|
EscapeCsv(MissingReference.MissingPackage.ToString()),
|
||||||
|
EscapeCsv(MissingReference.ReferencingAsset.GetSoftObjectPath().ToString()),
|
||||||
|
EscapeCsv(DependencyTypeToString(MissingReference.DependencyType)),
|
||||||
|
};
|
||||||
|
Rows.Add(FString::Join(Columns, TEXT(",")));
|
||||||
|
}
|
||||||
|
|
||||||
|
Rows.Add(TEXT(""));
|
||||||
|
Rows.Add(TEXT("Dependency Cycle"));
|
||||||
|
for (const FDirectiveUtilAssetDependencyCycle& Cycle : Report.DependencyCycles)
|
||||||
|
{
|
||||||
|
TArray<FString> Packages;
|
||||||
|
for (const FName Package : Cycle.Packages)
|
||||||
|
{
|
||||||
|
Packages.Add(Package.ToString());
|
||||||
|
}
|
||||||
|
Rows.Add(EscapeCsv(FString::Join(Packages, TEXT(" -> "))));
|
||||||
|
}
|
||||||
|
|
||||||
|
return FString::Join(Rows, TEXT("\n"));
|
||||||
|
}
|
||||||
@@ -2,14 +2,19 @@
|
|||||||
|
|
||||||
|
|
||||||
#include "Libraries/DirectiveUtilEditorAssetLibrary.h"
|
#include "Libraries/DirectiveUtilEditorAssetLibrary.h"
|
||||||
|
#include "AssetRegistry/DirectiveUtilAssetRegistry.h"
|
||||||
|
#include "Editor.h"
|
||||||
#include "Subsystems/EditorAssetSubsystem.h"
|
#include "Subsystems/EditorAssetSubsystem.h"
|
||||||
#include "Algo/Transform.h"
|
#include "Algo/Transform.h"
|
||||||
#include "AssetRegistry/IAssetRegistry.h"
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
#include "AssetRegistry/ARFilter.h"
|
#include "AssetRegistry/ARFilter.h"
|
||||||
#include "AssetToolsModule.h"
|
#include "AssetToolsModule.h"
|
||||||
|
#include "Editor.h"
|
||||||
#include "IAssetTools.h"
|
#include "IAssetTools.h"
|
||||||
|
#include "Misc/App.h"
|
||||||
#include "Misc/AssetRegistryInterface.h"
|
#include "Misc/AssetRegistryInterface.h"
|
||||||
#include "Modules/ModuleManager.h"
|
#include "Modules/ModuleManager.h"
|
||||||
|
#include "Runtime/Launch/Resources/Version.h"
|
||||||
#include "UObject/ObjectRedirector.h"
|
#include "UObject/ObjectRedirector.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
@@ -18,10 +23,7 @@ namespace
|
|||||||
{
|
{
|
||||||
if (IAssetRegistry* AssetRegistry = IAssetRegistry::Get())
|
if (IAssetRegistry* AssetRegistry = IAssetRegistry::Get())
|
||||||
{
|
{
|
||||||
if (AssetRegistry->IsLoadingAssets())
|
DirectiveUtilitiesEditor::EnsureAssetRegistryScan(*AssetRegistry);
|
||||||
{
|
|
||||||
AssetRegistry->WaitForCompletion();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,10 +118,7 @@ EDirectiveUtilSuccessStatus UDirectiveUtilEditorAssetLibrary::FixUpRedirectorsIn
|
|||||||
return EDirectiveUtilSuccessStatus::Failure;
|
return EDirectiveUtilSuccessStatus::Failure;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (AssetRegistry->IsLoadingAssets())
|
DirectiveUtilitiesEditor::EnsureAssetRegistryScan(*AssetRegistry);
|
||||||
{
|
|
||||||
AssetRegistry->WaitForCompletion();
|
|
||||||
}
|
|
||||||
|
|
||||||
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
|
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
|
||||||
IAssetTools& AssetTools = AssetToolsModule.Get();
|
IAssetTools& AssetTools = AssetToolsModule.Get();
|
||||||
@@ -160,6 +159,11 @@ EDirectiveUtilSuccessStatus UDirectiveUtilEditorAssetLibrary::FixUpRedirectorsIn
|
|||||||
{
|
{
|
||||||
return EDirectiveUtilSuccessStatus::Success;
|
return EDirectiveUtilSuccessStatus::Success;
|
||||||
}
|
}
|
||||||
|
if (FApp::IsUnattended())
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("Redirector fix-up requires an interactive editor session."));
|
||||||
|
return EDirectiveUtilSuccessStatus::Failure;
|
||||||
|
}
|
||||||
|
|
||||||
AssetTools.FixupReferencers(Redirectors, false, ERedirectFixupMode::DeleteFixedUpRedirectors);
|
AssetTools.FixupReferencers(Redirectors, false, ERedirectFixupMode::DeleteFixedUpRedirectors);
|
||||||
OutRedirectorsProcessed = Redirectors.Num();
|
OutRedirectorsProcessed = Redirectors.Num();
|
||||||
|
|||||||
@@ -0,0 +1,268 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "Libraries/DirectiveUtilEditorBlueprintLibrary.h"
|
||||||
|
|
||||||
|
#include "AssetRegistry/DirectiveUtilAssetRegistry.h"
|
||||||
|
#include "AssetRegistry/ARFilter.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "Blueprint/BlueprintSupport.h"
|
||||||
|
#include "Components/ActorComponent.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Engine/BlueprintGeneratedClass.h"
|
||||||
|
#include "Engine/SCS_Node.h"
|
||||||
|
#include "Engine/SimpleConstructionScript.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "Misc/PackageName.h"
|
||||||
|
#include "UObject/Package.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
bool IsPackageInsideBlueprintSearchPath(const FName PackageName, const FName Path)
|
||||||
|
{
|
||||||
|
const FString Package = PackageName.ToString();
|
||||||
|
FString Parent = Path.ToString();
|
||||||
|
Parent.RemoveFromEnd(TEXT("/"));
|
||||||
|
return Package == Parent || Package.StartsWith(Parent + TEXT("/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FAssetData> GetBlueprintAssets(const FDirectiveUtilBlueprintSearchOptions& Options)
|
||||||
|
{
|
||||||
|
IAssetRegistry* AssetRegistry = IAssetRegistry::Get();
|
||||||
|
if (!AssetRegistry)
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
DirectiveUtilitiesEditor::EnsureAssetRegistryScan(*AssetRegistry);
|
||||||
|
|
||||||
|
FARFilter Filter;
|
||||||
|
Filter.ClassPaths.Add(UBlueprint::StaticClass()->GetClassPathName());
|
||||||
|
Filter.bRecursiveClasses = true;
|
||||||
|
Filter.bRecursivePaths = true;
|
||||||
|
if (Options.PackagePaths.IsEmpty())
|
||||||
|
{
|
||||||
|
Filter.PackagePaths.Add(TEXT("/Game"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Filter.PackagePaths.Append(Options.PackagePaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FAssetData> Assets;
|
||||||
|
AssetRegistry->GetAssets(Filter, Assets);
|
||||||
|
Assets.RemoveAll([&Options](const FAssetData& Asset) {
|
||||||
|
for (const FName Path : Options.ExcludedPackagePaths)
|
||||||
|
{
|
||||||
|
if (IsPackageInsideBlueprintSearchPath(Asset.PackageName, Path))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
Assets.Sort([](const FAssetData& Left, const FAssetData& Right) {
|
||||||
|
return Left.GetSoftObjectPath().LexicalLess(Right.GetSoftObjectPath());
|
||||||
|
});
|
||||||
|
return Assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
UBlueprint* LoadBlueprint(const FAssetData& Asset)
|
||||||
|
{
|
||||||
|
return Cast<UBlueprint>(Asset.GetAsset());
|
||||||
|
}
|
||||||
|
|
||||||
|
FString GetClassObjectPath(const UClass* Class)
|
||||||
|
{
|
||||||
|
return Class ? Class->GetPathName() : FString();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsDirectParent(const FAssetData& Asset, const UClass* ParentClass)
|
||||||
|
{
|
||||||
|
const FString ParentTag = Asset.GetTagValueRef<FString>(FBlueprintTags::ParentClassPath);
|
||||||
|
return FPackageName::ExportTextPathToObjectPath(ParentTag) == GetClassObjectPath(ParentClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasComponentClass(const UBlueprint* Blueprint, const UClass* ComponentClass, const bool bIncludeDerivedComponents)
|
||||||
|
{
|
||||||
|
TSet<const UBlueprint*> Visited;
|
||||||
|
for (const UBlueprint* Current = Blueprint; Current && !Visited.Contains(Current);)
|
||||||
|
{
|
||||||
|
Visited.Add(Current);
|
||||||
|
if (Current->SimpleConstructionScript)
|
||||||
|
{
|
||||||
|
for (const USCS_Node* Node : Current->SimpleConstructionScript->GetAllNodes())
|
||||||
|
{
|
||||||
|
if (!Node || !Node->ComponentClass)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool bMatches = bIncludeDerivedComponents
|
||||||
|
? Node->ComponentClass->IsChildOf(ComponentClass)
|
||||||
|
: Node->ComponentClass == ComponentClass;
|
||||||
|
if (bMatches)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Current = Current->ParentClass ? UBlueprint::GetBlueprintFromClass(Current->ParentClass) : nullptr;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EDirectiveUtilBlueprintCompileStatus UDirectiveUtilEditorBlueprintLibrary::GetBlueprintCompileStatus(
|
||||||
|
const UBlueprint* Blueprint)
|
||||||
|
{
|
||||||
|
if (!IsValid(Blueprint))
|
||||||
|
{
|
||||||
|
return EDirectiveUtilBlueprintCompileStatus::Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (Blueprint->Status)
|
||||||
|
{
|
||||||
|
case BS_Dirty:
|
||||||
|
return EDirectiveUtilBlueprintCompileStatus::Dirty;
|
||||||
|
case BS_Error:
|
||||||
|
return EDirectiveUtilBlueprintCompileStatus::Error;
|
||||||
|
case BS_UpToDate:
|
||||||
|
return EDirectiveUtilBlueprintCompileStatus::UpToDate;
|
||||||
|
case BS_BeingCreated:
|
||||||
|
return EDirectiveUtilBlueprintCompileStatus::BeingCreated;
|
||||||
|
case BS_UpToDateWithWarnings:
|
||||||
|
return EDirectiveUtilBlueprintCompileStatus::UpToDateWithWarnings;
|
||||||
|
case BS_Unknown:
|
||||||
|
default:
|
||||||
|
return EDirectiveUtilBlueprintCompileStatus::Unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FAssetData> UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsByCompileStatus(
|
||||||
|
const EDirectiveUtilBlueprintCompileStatus CompileStatus,
|
||||||
|
const FDirectiveUtilBlueprintSearchOptions& Options)
|
||||||
|
{
|
||||||
|
TArray<FAssetData> Matches;
|
||||||
|
for (const FAssetData& Asset : GetBlueprintAssets(Options))
|
||||||
|
{
|
||||||
|
if (const UBlueprint* Blueprint = LoadBlueprint(Asset))
|
||||||
|
{
|
||||||
|
if (GetBlueprintCompileStatus(Blueprint) == CompileStatus)
|
||||||
|
{
|
||||||
|
Matches.Add(Asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FAssetData> UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsByParentClass(
|
||||||
|
UClass* ParentClass,
|
||||||
|
const FDirectiveUtilBlueprintSearchOptions& Options,
|
||||||
|
const bool bIncludeDescendants)
|
||||||
|
{
|
||||||
|
TArray<FAssetData> Matches;
|
||||||
|
if (!IsValid(ParentClass))
|
||||||
|
{
|
||||||
|
return Matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const FAssetData& Asset : GetBlueprintAssets(Options))
|
||||||
|
{
|
||||||
|
if (!bIncludeDescendants)
|
||||||
|
{
|
||||||
|
if (IsDirectParent(Asset, ParentClass))
|
||||||
|
{
|
||||||
|
Matches.Add(Asset);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UBlueprint* Blueprint = LoadBlueprint(Asset);
|
||||||
|
if (Blueprint && Blueprint->ParentClass && Blueprint->ParentClass->IsChildOf(ParentClass))
|
||||||
|
{
|
||||||
|
Matches.Add(Asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FAssetData> UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsImplementingInterface(
|
||||||
|
UClass* InterfaceClass,
|
||||||
|
const FDirectiveUtilBlueprintSearchOptions& Options)
|
||||||
|
{
|
||||||
|
TArray<FAssetData> Matches;
|
||||||
|
if (!IsValid(InterfaceClass) || !InterfaceClass->HasAnyClassFlags(CLASS_Interface))
|
||||||
|
{
|
||||||
|
return Matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FString InterfacePath = GetClassObjectPath(InterfaceClass);
|
||||||
|
for (const FAssetData& Asset : GetBlueprintAssets(Options))
|
||||||
|
{
|
||||||
|
const FString Interfaces = Asset.GetTagValueRef<FString>(FBlueprintTags::ImplementedInterfaces);
|
||||||
|
const FString ParentTag = Asset.GetTagValueRef<FString>(FBlueprintTags::ParentClassPath);
|
||||||
|
UClass* TaggedParent = FindObject<UClass>(nullptr, *FPackageName::ExportTextPathToObjectPath(ParentTag));
|
||||||
|
if (!Interfaces.Contains(InterfacePath) && TaggedParent && !TaggedParent->ImplementsInterface(InterfaceClass))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UBlueprint* Blueprint = LoadBlueprint(Asset);
|
||||||
|
if (Blueprint && Blueprint->GeneratedClass && Blueprint->GeneratedClass->ImplementsInterface(InterfaceClass))
|
||||||
|
{
|
||||||
|
Matches.Add(Asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FAssetData> UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsContainingComponentClass(
|
||||||
|
UClass* ComponentClass,
|
||||||
|
const FDirectiveUtilBlueprintSearchOptions& Options,
|
||||||
|
const bool bIncludeDerivedComponents)
|
||||||
|
{
|
||||||
|
TArray<FAssetData> Matches;
|
||||||
|
if (!IsValid(ComponentClass) || !ComponentClass->IsChildOf(UActorComponent::StaticClass()))
|
||||||
|
{
|
||||||
|
return Matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const FAssetData& Asset : GetBlueprintAssets(Options))
|
||||||
|
{
|
||||||
|
if (const UBlueprint* Blueprint = LoadBlueprint(Asset))
|
||||||
|
{
|
||||||
|
if (HasComponentClass(Blueprint, ComponentClass, bIncludeDerivedComponents))
|
||||||
|
{
|
||||||
|
Matches.Add(Asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FName> UDirectiveUtilEditorBlueprintLibrary::GetUnusedBlueprintVariables(
|
||||||
|
UBlueprint* Blueprint,
|
||||||
|
const bool bIncludeExternallyAccessibleVariables)
|
||||||
|
{
|
||||||
|
TArray<FName> Names;
|
||||||
|
if (!IsValid(Blueprint))
|
||||||
|
{
|
||||||
|
return Names;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FProperty*> UsedVariables;
|
||||||
|
TArray<FProperty*> UnusedVariables;
|
||||||
|
FBlueprintEditorUtils::GetUsedAndUnusedVariables(Blueprint, UsedVariables, UnusedVariables);
|
||||||
|
Names.Reserve(UnusedVariables.Num());
|
||||||
|
for (const FProperty* Variable : UnusedVariables)
|
||||||
|
{
|
||||||
|
if (Variable && (bIncludeExternallyAccessibleVariables || FBlueprintEditorUtils::IsPropertyPrivate(Variable)))
|
||||||
|
{
|
||||||
|
Names.Add(Variable->GetFName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Names.Sort(FNameLexicalLess());
|
||||||
|
return Names;
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "Libraries/DirectiveUtilEditorTaskLibrary.h"
|
||||||
|
|
||||||
|
#include "Framework/Application/SlateApplication.h"
|
||||||
|
#include "Framework/Notifications/NotificationManager.h"
|
||||||
|
#include "Styling/AppStyle.h"
|
||||||
|
#include "Tasks/DirectiveUtilEditorSlowTask.h"
|
||||||
|
#include "Widgets/Notifications/SNotificationList.h"
|
||||||
|
|
||||||
|
UDirectiveUtilEditorSlowTask* UDirectiveUtilEditorTaskLibrary::StartEditorSlowTask(
|
||||||
|
const float TotalWork,
|
||||||
|
const FText& Description,
|
||||||
|
const bool bCanCancel)
|
||||||
|
{
|
||||||
|
if (!FMath::IsFinite(TotalWork) || TotalWork <= 0.0f)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
UDirectiveUtilEditorSlowTask* Task = NewObject<UDirectiveUtilEditorSlowTask>();
|
||||||
|
Task->Initialize(TotalWork, Description, bCanCancel, true);
|
||||||
|
return Task->IsActive() ? Task : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDirectiveUtilEditorTaskLibrary::ShowEditorNotification(
|
||||||
|
const FText& Message,
|
||||||
|
const EDirectiveUtilEditorNotificationState State,
|
||||||
|
const float ExpireDuration)
|
||||||
|
{
|
||||||
|
if (!FSlateApplication::IsInitialized())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FNotificationInfo NotificationInfo(Message);
|
||||||
|
NotificationInfo.bFireAndForget = true;
|
||||||
|
NotificationInfo.ExpireDuration = FMath::IsFinite(ExpireDuration)
|
||||||
|
? FMath::Max(0.0f, ExpireDuration)
|
||||||
|
: 0.0f;
|
||||||
|
if (State == EDirectiveUtilEditorNotificationState::Warning)
|
||||||
|
{
|
||||||
|
NotificationInfo.Image = FAppStyle::GetBrush("Icons.WarningWithColor");
|
||||||
|
}
|
||||||
|
|
||||||
|
const TSharedPtr<SNotificationItem> Notification = FSlateNotificationManager::Get().AddNotification(NotificationInfo);
|
||||||
|
if (!Notification.IsValid())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (State)
|
||||||
|
{
|
||||||
|
case EDirectiveUtilEditorNotificationState::Success:
|
||||||
|
Notification->SetCompletionState(SNotificationItem::CS_Success);
|
||||||
|
break;
|
||||||
|
case EDirectiveUtilEditorNotificationState::Failure:
|
||||||
|
Notification->SetCompletionState(SNotificationItem::CS_Fail);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Notification->SetCompletionState(SNotificationItem::CS_None);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -3,17 +3,47 @@
|
|||||||
#include "Subsystems/DirectiveUtilEditorActorSubsystem.h"
|
#include "Subsystems/DirectiveUtilEditorActorSubsystem.h"
|
||||||
|
|
||||||
#include "DirectiveUtilLogChannels.h"
|
#include "DirectiveUtilLogChannels.h"
|
||||||
|
#include "Components/StaticMeshComponent.h"
|
||||||
|
#include "Editor.h"
|
||||||
#include "Engine/StaticMeshActor.h"
|
#include "Engine/StaticMeshActor.h"
|
||||||
|
#include "Engine/StaticMesh.h"
|
||||||
|
#include "Engine/Texture.h"
|
||||||
|
#include "Engine/Texture2D.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "Materials/MaterialInterface.h"
|
||||||
#include "Materials/MaterialExpressionTextureObject.h"
|
#include "Materials/MaterialExpressionTextureObject.h"
|
||||||
#include "EditorViewportClient.h"
|
#include "EditorViewportClient.h"
|
||||||
|
#include "LevelEditorViewport.h"
|
||||||
#include "Components/BoxComponent.h"
|
#include "Components/BoxComponent.h"
|
||||||
#include "Components/CapsuleComponent.h"
|
#include "Components/CapsuleComponent.h"
|
||||||
#include "Components/SphereComponent.h"
|
#include "Components/SphereComponent.h"
|
||||||
#include "Materials/MaterialExpressionTextureSample.h"
|
#include "Materials/MaterialExpressionTextureSample.h"
|
||||||
|
#include "Runtime/Launch/Resources/Version.h"
|
||||||
#include "ScopedTransaction.h"
|
#include "ScopedTransaction.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
bool IsFiniteVector(const FVector& Value)
|
||||||
|
{
|
||||||
|
return FMath::IsFinite(Value.X) && FMath::IsFinite(Value.Y) && FMath::IsFinite(Value.Z);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsFiniteQuat(const FQuat& Value)
|
||||||
|
{
|
||||||
|
return FMath::IsFinite(Value.X)
|
||||||
|
&& FMath::IsFinite(Value.Y)
|
||||||
|
&& FMath::IsFinite(Value.Z)
|
||||||
|
&& FMath::IsFinite(Value.W);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MatchesTextureReference(const UTexture* Texture, const TSoftObjectPtr<UTexture2D>& TextureReference)
|
||||||
|
{
|
||||||
|
return TextureReference.IsNull()
|
||||||
|
? Texture == nullptr
|
||||||
|
: Texture && FSoftObjectPath(Texture) == TextureReference.ToSoftObjectPath();
|
||||||
|
}
|
||||||
|
|
||||||
// Adds an actor to OutActors when whether it matches the predicate (evaluated once across all of the
|
// Adds an actor to OutActors when whether it matches the predicate (evaluated once across all of the
|
||||||
// actor's components) equals whether matches are being included. Centralizes the include/exclude
|
// actor's components) equals whether matches are being included. Centralizes the include/exclude
|
||||||
// aggregation so a multi-component actor is judged per-actor rather than per-component.
|
// aggregation so a multi-component actor is judged per-actor rather than per-component.
|
||||||
@@ -39,7 +69,7 @@ namespace
|
|||||||
|
|
||||||
for (TSourceActor* Actor : *Source)
|
for (TSourceActor* Actor : *Source)
|
||||||
{
|
{
|
||||||
if (!Actor || Seen.Contains(Actor)) { continue; }
|
if (!IsValid(Actor) || Seen.Contains(Actor)) { continue; }
|
||||||
if (ActorMatches(Actor) == bIncludeMatches)
|
if (ActorMatches(Actor) == bIncludeMatches)
|
||||||
{
|
{
|
||||||
Seen.Add(Actor);
|
Seen.Add(Actor);
|
||||||
@@ -47,27 +77,163 @@ namespace
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct FActorLayoutData
|
||||||
|
{
|
||||||
|
AActor* Actor = nullptr;
|
||||||
|
FBox Bounds = FBox(ForceInit);
|
||||||
|
FVector Location = FVector::ZeroVector;
|
||||||
|
int32 AttachmentDepth = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
float GetAxisValue(const FVector& Vector, const EDirectiveUtilActorLayoutAxis Axis)
|
||||||
|
{
|
||||||
|
switch (Axis)
|
||||||
|
{
|
||||||
|
case EDirectiveUtilActorLayoutAxis::X:
|
||||||
|
return Vector.X;
|
||||||
|
case EDirectiveUtilActorLayoutAxis::Y:
|
||||||
|
return Vector.Y;
|
||||||
|
case EDirectiveUtilActorLayoutAxis::Z:
|
||||||
|
return Vector.Z;
|
||||||
|
}
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetAxisValue(FVector& Vector, const EDirectiveUtilActorLayoutAxis Axis, const float Value)
|
||||||
|
{
|
||||||
|
switch (Axis)
|
||||||
|
{
|
||||||
|
case EDirectiveUtilActorLayoutAxis::X:
|
||||||
|
Vector.X = Value;
|
||||||
|
break;
|
||||||
|
case EDirectiveUtilActorLayoutAxis::Y:
|
||||||
|
Vector.Y = Value;
|
||||||
|
break;
|
||||||
|
case EDirectiveUtilActorLayoutAxis::Z:
|
||||||
|
Vector.Z = Value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 GetAttachmentDepth(const AActor* Actor)
|
||||||
|
{
|
||||||
|
int32 Depth = 0;
|
||||||
|
TSet<const AActor*> Visited;
|
||||||
|
for (const AActor* Parent = Actor ? Actor->GetAttachParentActor() : nullptr;
|
||||||
|
Parent && !Visited.Contains(Parent);
|
||||||
|
Parent = Parent->GetAttachParentActor())
|
||||||
|
{
|
||||||
|
Visited.Add(Parent);
|
||||||
|
++Depth;
|
||||||
|
}
|
||||||
|
return Depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsLayoutActorValid(AActor* Actor)
|
||||||
|
{
|
||||||
|
return IsValid(Actor)
|
||||||
|
&& Actor->GetWorld()
|
||||||
|
&& !Actor->HasAnyFlags(RF_Transient)
|
||||||
|
&& !Actor->IsActorBeingDestroyed();
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FActorLayoutData> GetLayoutActors(
|
||||||
|
const TArray<AActor*>& Actors,
|
||||||
|
FDirectiveUtilActorOperationResult& Result)
|
||||||
|
{
|
||||||
|
TArray<FActorLayoutData> LayoutActors;
|
||||||
|
TSet<AActor*> Seen;
|
||||||
|
for (AActor* Actor : Actors)
|
||||||
|
{
|
||||||
|
if (!Actor || Seen.Contains(Actor))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Seen.Add(Actor);
|
||||||
|
|
||||||
|
if (!IsLayoutActorValid(Actor))
|
||||||
|
{
|
||||||
|
Result.SkippedActors.Add(Actor);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
FActorLayoutData Data;
|
||||||
|
Data.Actor = Actor;
|
||||||
|
Data.Location = Actor->GetActorLocation();
|
||||||
|
if (!IsFiniteVector(Data.Location)
|
||||||
|
|| !IsFiniteVector(Actor->GetActorScale3D())
|
||||||
|
|| !IsFiniteQuat(Actor->GetActorQuat()))
|
||||||
|
{
|
||||||
|
Result.SkippedActors.Add(Actor);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Data.Bounds = Actor->GetComponentsBoundingBox(true, true);
|
||||||
|
if (!Data.Bounds.IsValid)
|
||||||
|
{
|
||||||
|
Data.Bounds = FBox(Data.Location, Data.Location);
|
||||||
|
}
|
||||||
|
if (!IsFiniteVector(Data.Bounds.Min) || !IsFiniteVector(Data.Bounds.Max))
|
||||||
|
{
|
||||||
|
Result.SkippedActors.Add(Actor);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Data.AttachmentDepth = GetAttachmentDepth(Actor);
|
||||||
|
LayoutActors.Add(MoveTemp(Data));
|
||||||
|
}
|
||||||
|
return LayoutActors;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApplyLocations(
|
||||||
|
TArray<FActorLayoutData>& LayoutActors,
|
||||||
|
const TMap<AActor*, FVector>& Locations,
|
||||||
|
FDirectiveUtilActorOperationResult& Result)
|
||||||
|
{
|
||||||
|
LayoutActors.Sort([](const FActorLayoutData& Left, const FActorLayoutData& Right) {
|
||||||
|
return Left.AttachmentDepth < Right.AttachmentDepth;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const FActorLayoutData& Data : LayoutActors)
|
||||||
|
{
|
||||||
|
const FVector* Location = Locations.Find(Data.Actor);
|
||||||
|
if (!Location || !IsFiniteVector(*Location) || Data.Actor->GetActorLocation().Equals(*Location))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Data.Actor->Modify();
|
||||||
|
if (Data.Actor->SetActorLocation(*Location, false, nullptr, ETeleportType::TeleportPhysics))
|
||||||
|
{
|
||||||
|
Result.ChangedActors.Add(Data.Actor);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Result.SkippedActors.Add(Data.Actor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UDirectiveUtilEditorActorSubsystem::FocusActorsInViewport(const TArray<AActor*> Actors, const bool bInstant)
|
void UDirectiveUtilEditorActorSubsystem::FocusActorsInViewport(const TArray<AActor*> Actors, const bool bInstant)
|
||||||
{
|
{
|
||||||
if (Actors.Num() == 0) { return; }
|
if (!GCurrentLevelEditingViewportClient) { return; }
|
||||||
if (!GEditor) { return; }
|
|
||||||
|
|
||||||
FViewport* ActiveViewport = GEditor->GetActiveViewport();
|
|
||||||
if (!ActiveViewport) { return; }
|
|
||||||
|
|
||||||
FEditorViewportClient* ViewportClient = static_cast<FEditorViewportClient*>(ActiveViewport->GetClient());
|
|
||||||
if (!ViewportClient) { return; }
|
|
||||||
|
|
||||||
FBox BoundingBox = FBox(ForceInit);
|
FBox BoundingBox = FBox(ForceInit);
|
||||||
for (const auto Actor : Actors)
|
for (const AActor* Actor : Actors)
|
||||||
{
|
{
|
||||||
if (!Actor) { continue; }
|
if (!IsValid(Actor)) { continue; }
|
||||||
BoundingBox += Actor->GetComponentsBoundingBox(true, true);
|
const FBox ActorBounds = Actor->GetComponentsBoundingBox(true, true);
|
||||||
|
if (ActorBounds.IsValid)
|
||||||
|
{
|
||||||
|
BoundingBox += ActorBounds;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewportClient->FocusViewportOnBox(BoundingBox, bInstant);
|
if (BoundingBox.IsValid)
|
||||||
|
{
|
||||||
|
GCurrentLevelEditingViewportClient->FocusViewportOnBox(BoundingBox, bInstant);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TArray<UClass*> UDirectiveUtilEditorActorSubsystem::GetAllLevelClasses()
|
TArray<UClass*> UDirectiveUtilEditorActorSubsystem::GetAllLevelClasses()
|
||||||
@@ -84,6 +250,236 @@ TArray<UClass*> UDirectiveUtilEditorActorSubsystem::GetAllLevelClasses()
|
|||||||
return ActorClasses;
|
return ActorClasses;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FDirectiveUtilActorOperationResult UDirectiveUtilEditorActorSubsystem::AlignActors(
|
||||||
|
const TArray<AActor*>& Actors,
|
||||||
|
const EDirectiveUtilActorLayoutAxis Axis,
|
||||||
|
const EDirectiveUtilActorAlignment Alignment)
|
||||||
|
{
|
||||||
|
FDirectiveUtilActorOperationResult Result;
|
||||||
|
TArray<FActorLayoutData> LayoutActors = GetLayoutActors(Actors, Result);
|
||||||
|
if (LayoutActors.Num() < 2)
|
||||||
|
{
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
FBox CombinedBounds(ForceInit);
|
||||||
|
for (const FActorLayoutData& Data : LayoutActors)
|
||||||
|
{
|
||||||
|
CombinedBounds += Data.Bounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
float Target = 0.0f;
|
||||||
|
switch (Alignment)
|
||||||
|
{
|
||||||
|
case EDirectiveUtilActorAlignment::Minimum:
|
||||||
|
Target = GetAxisValue(CombinedBounds.Min, Axis);
|
||||||
|
break;
|
||||||
|
case EDirectiveUtilActorAlignment::Center:
|
||||||
|
Target = GetAxisValue(CombinedBounds.GetCenter(), Axis);
|
||||||
|
break;
|
||||||
|
case EDirectiveUtilActorAlignment::Maximum:
|
||||||
|
Target = GetAxisValue(CombinedBounds.Max, Axis);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
TMap<AActor*, FVector> Locations;
|
||||||
|
for (const FActorLayoutData& Data : LayoutActors)
|
||||||
|
{
|
||||||
|
const float Current = Alignment == EDirectiveUtilActorAlignment::Minimum
|
||||||
|
? GetAxisValue(Data.Bounds.Min, Axis)
|
||||||
|
: Alignment == EDirectiveUtilActorAlignment::Maximum
|
||||||
|
? GetAxisValue(Data.Bounds.Max, Axis)
|
||||||
|
: GetAxisValue(Data.Bounds.GetCenter(), Axis);
|
||||||
|
FVector Location = Data.Location;
|
||||||
|
SetAxisValue(Location, Axis, GetAxisValue(Location, Axis) + Target - Current);
|
||||||
|
Locations.Add(Data.Actor, Location);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FScopedTransaction Transaction(NSLOCTEXT("DirectiveUtilities", "AlignActors", "Align Actors"));
|
||||||
|
ApplyLocations(LayoutActors, Locations, Result);
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
FDirectiveUtilActorOperationResult UDirectiveUtilEditorActorSubsystem::DistributeActors(
|
||||||
|
const TArray<AActor*>& Actors,
|
||||||
|
const EDirectiveUtilActorLayoutAxis Axis,
|
||||||
|
const EDirectiveUtilActorDistribution Distribution)
|
||||||
|
{
|
||||||
|
FDirectiveUtilActorOperationResult Result;
|
||||||
|
TArray<FActorLayoutData> LayoutActors = GetLayoutActors(Actors, Result);
|
||||||
|
if (LayoutActors.Num() < 3)
|
||||||
|
{
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
LayoutActors.Sort([Axis](const FActorLayoutData& Left, const FActorLayoutData& Right) {
|
||||||
|
return GetAxisValue(Left.Bounds.GetCenter(), Axis) < GetAxisValue(Right.Bounds.GetCenter(), Axis);
|
||||||
|
});
|
||||||
|
|
||||||
|
TMap<AActor*, FVector> Locations;
|
||||||
|
if (Distribution == EDirectiveUtilActorDistribution::Centers)
|
||||||
|
{
|
||||||
|
const float Start = GetAxisValue(LayoutActors[0].Bounds.GetCenter(), Axis);
|
||||||
|
const float End = GetAxisValue(LayoutActors.Last().Bounds.GetCenter(), Axis);
|
||||||
|
const float Spacing = (End - Start) / (LayoutActors.Num() - 1);
|
||||||
|
for (int32 Index = 1; Index < LayoutActors.Num() - 1; ++Index)
|
||||||
|
{
|
||||||
|
const FActorLayoutData& Data = LayoutActors[Index];
|
||||||
|
FVector Location = Data.Location;
|
||||||
|
const float Delta = Start + Spacing * Index - GetAxisValue(Data.Bounds.GetCenter(), Axis);
|
||||||
|
SetAxisValue(Location, Axis, GetAxisValue(Location, Axis) + Delta);
|
||||||
|
Locations.Add(Data.Actor, Location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float TotalSize = 0.0f;
|
||||||
|
for (const FActorLayoutData& Data : LayoutActors)
|
||||||
|
{
|
||||||
|
TotalSize += GetAxisValue(Data.Bounds.GetSize(), Axis);
|
||||||
|
}
|
||||||
|
const float Start = GetAxisValue(LayoutActors[0].Bounds.Min, Axis);
|
||||||
|
const float End = GetAxisValue(LayoutActors.Last().Bounds.Max, Axis);
|
||||||
|
const float Gap = (End - Start - TotalSize) / (LayoutActors.Num() - 1);
|
||||||
|
float NextMinimum = GetAxisValue(LayoutActors[0].Bounds.Max, Axis) + Gap;
|
||||||
|
for (int32 Index = 1; Index < LayoutActors.Num() - 1; ++Index)
|
||||||
|
{
|
||||||
|
const FActorLayoutData& Data = LayoutActors[Index];
|
||||||
|
FVector Location = Data.Location;
|
||||||
|
const float Delta = NextMinimum - GetAxisValue(Data.Bounds.Min, Axis);
|
||||||
|
SetAxisValue(Location, Axis, GetAxisValue(Location, Axis) + Delta);
|
||||||
|
Locations.Add(Data.Actor, Location);
|
||||||
|
NextMinimum += GetAxisValue(Data.Bounds.GetSize(), Axis) + Gap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FScopedTransaction Transaction(NSLOCTEXT("DirectiveUtilities", "DistributeActors", "Distribute Actors"));
|
||||||
|
ApplyLocations(LayoutActors, Locations, Result);
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
FDirectiveUtilActorOperationResult UDirectiveUtilEditorActorSubsystem::SnapActorsToSurface(
|
||||||
|
const TArray<AActor*>& Actors,
|
||||||
|
const FVector TraceDirection,
|
||||||
|
const float MaximumDistance,
|
||||||
|
const TEnumAsByte<ECollisionChannel> TraceChannel,
|
||||||
|
const EDirectiveUtilSurfacePlacement Placement,
|
||||||
|
const bool bAlignToNormal)
|
||||||
|
{
|
||||||
|
FDirectiveUtilActorOperationResult Result;
|
||||||
|
TArray<FActorLayoutData> LayoutActors = GetLayoutActors(Actors, Result);
|
||||||
|
if (LayoutActors.IsEmpty()
|
||||||
|
|| !FMath::IsFinite(MaximumDistance)
|
||||||
|
|| MaximumDistance <= 0.0f
|
||||||
|
|| !IsFiniteVector(TraceDirection)
|
||||||
|
|| TraceDirection.IsNearlyZero()
|
||||||
|
|| TraceChannel.GetValue() >= ECC_MAX)
|
||||||
|
{
|
||||||
|
for (const FActorLayoutData& Data : LayoutActors)
|
||||||
|
{
|
||||||
|
Result.SkippedActors.Add(Data.Actor);
|
||||||
|
}
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FVector Direction = TraceDirection.GetSafeNormal();
|
||||||
|
FCollisionQueryParams QueryParams(SCENE_QUERY_STAT(DirectiveUtilitiesSnapActors), true);
|
||||||
|
for (const FActorLayoutData& Data : LayoutActors)
|
||||||
|
{
|
||||||
|
QueryParams.AddIgnoredActor(Data.Actor);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FSnapTarget
|
||||||
|
{
|
||||||
|
FVector Location;
|
||||||
|
FQuat Rotation;
|
||||||
|
};
|
||||||
|
TMap<AActor*, FSnapTarget> Targets;
|
||||||
|
for (const FActorLayoutData& Data : LayoutActors)
|
||||||
|
{
|
||||||
|
FHitResult Hit;
|
||||||
|
const FVector Start = Data.Location;
|
||||||
|
if (!Data.Actor->GetWorld()->LineTraceSingleByChannel(
|
||||||
|
Hit,
|
||||||
|
Start,
|
||||||
|
Start + Direction * MaximumDistance,
|
||||||
|
TraceChannel,
|
||||||
|
QueryParams))
|
||||||
|
{
|
||||||
|
Result.SkippedActors.Add(Data.Actor);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!IsFiniteVector(Hit.Location)
|
||||||
|
|| (bAlignToNormal && (!IsFiniteVector(Hit.ImpactNormal) || Hit.ImpactNormal.IsNearlyZero())))
|
||||||
|
{
|
||||||
|
Result.SkippedActors.Add(Data.Actor);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector Location = Hit.Location;
|
||||||
|
if (Placement == EDirectiveUtilSurfacePlacement::Bounds)
|
||||||
|
{
|
||||||
|
const FVector Extent = Data.Bounds.GetExtent();
|
||||||
|
const float Support = FMath::Abs(Direction.X) * Extent.X
|
||||||
|
+ FMath::Abs(Direction.Y) * Extent.Y
|
||||||
|
+ FMath::Abs(Direction.Z) * Extent.Z;
|
||||||
|
Location = Hit.Location - Direction * Support - (Data.Bounds.GetCenter() - Data.Location);
|
||||||
|
}
|
||||||
|
|
||||||
|
FQuat Rotation = Data.Actor->GetActorQuat();
|
||||||
|
if (bAlignToNormal)
|
||||||
|
{
|
||||||
|
Rotation = FQuat::FindBetweenNormals(Rotation.GetUpVector(), Hit.ImpactNormal) * Rotation;
|
||||||
|
Rotation.Normalize();
|
||||||
|
}
|
||||||
|
if (IsFiniteVector(Location) && IsFiniteQuat(Rotation))
|
||||||
|
{
|
||||||
|
Targets.Add(Data.Actor, {Location, Rotation});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Result.SkippedActors.Add(Data.Actor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LayoutActors.Sort([](const FActorLayoutData& Left, const FActorLayoutData& Right) {
|
||||||
|
return Left.AttachmentDepth < Right.AttachmentDepth;
|
||||||
|
});
|
||||||
|
const FScopedTransaction Transaction(NSLOCTEXT("DirectiveUtilities", "SnapActorsToSurface", "Snap Actors To Surface"));
|
||||||
|
for (const FActorLayoutData& Data : LayoutActors)
|
||||||
|
{
|
||||||
|
const FSnapTarget* Target = Targets.Find(Data.Actor);
|
||||||
|
if (!Target)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool bLocationChanged = !Data.Actor->GetActorLocation().Equals(Target->Location);
|
||||||
|
const bool bRotationChanged = !Data.Actor->GetActorQuat().Equals(Target->Rotation);
|
||||||
|
if (!bLocationChanged && !bRotationChanged)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Data.Actor->Modify();
|
||||||
|
const bool bMoved = Data.Actor->SetActorLocationAndRotation(
|
||||||
|
Target->Location,
|
||||||
|
Target->Rotation,
|
||||||
|
false,
|
||||||
|
nullptr,
|
||||||
|
ETeleportType::TeleportPhysics);
|
||||||
|
if (bMoved)
|
||||||
|
{
|
||||||
|
Result.ChangedActors.Add(Data.Actor);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Result.SkippedActors.Add(Data.Actor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
void UDirectiveUtilEditorActorSubsystem::FilterStaticMeshActors(
|
void UDirectiveUtilEditorActorSubsystem::FilterStaticMeshActors(
|
||||||
TArray<AStaticMeshActor*>& OutStaticMeshActors,
|
TArray<AStaticMeshActor*>& OutStaticMeshActors,
|
||||||
TArray<AActor*> ActorsToFilter) const
|
TArray<AActor*> ActorsToFilter) const
|
||||||
@@ -735,11 +1131,11 @@ void UDirectiveUtilEditorActorSubsystem::FilterActorsByTexture(
|
|||||||
{
|
{
|
||||||
if (const UMaterialExpressionTextureSample* TextureSample = Cast<UMaterialExpressionTextureSample>(Expression))
|
if (const UMaterialExpressionTextureSample* TextureSample = Cast<UMaterialExpressionTextureSample>(Expression))
|
||||||
{
|
{
|
||||||
if (TextureSample->Texture == TextureReference) { return true; }
|
if (MatchesTextureReference(TextureSample->Texture, TextureReference)) { return true; }
|
||||||
}
|
}
|
||||||
if (const UMaterialExpressionTextureObject* TextureObject = Cast<UMaterialExpressionTextureObject>(Expression))
|
if (const UMaterialExpressionTextureObject* TextureObject = Cast<UMaterialExpressionTextureObject>(Expression))
|
||||||
{
|
{
|
||||||
if (TextureObject->Texture == TextureReference) { return true; }
|
if (MatchesTextureReference(TextureObject->Texture, TextureReference)) { return true; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -757,11 +1153,11 @@ void UDirectiveUtilEditorActorSubsystem::FilterActorsByTexture(
|
|||||||
{
|
{
|
||||||
if (const UMaterialExpressionTextureSample* TextureSample = Cast<UMaterialExpressionTextureSample>(Expression))
|
if (const UMaterialExpressionTextureSample* TextureSample = Cast<UMaterialExpressionTextureSample>(Expression))
|
||||||
{
|
{
|
||||||
if (TextureSample->Texture == TextureReference) { return true; }
|
if (MatchesTextureReference(TextureSample->Texture, TextureReference)) { return true; }
|
||||||
}
|
}
|
||||||
if (const UMaterialExpressionTextureObject* TextureObject = Cast<UMaterialExpressionTextureObject>(Expression))
|
if (const UMaterialExpressionTextureObject* TextureObject = Cast<UMaterialExpressionTextureObject>(Expression))
|
||||||
{
|
{
|
||||||
if (TextureObject->Texture == TextureReference) { return true; }
|
if (MatchesTextureReference(TextureObject->Texture, TextureReference)) { return true; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -901,36 +1297,7 @@ void UDirectiveUtilEditorActorSubsystem::GetActorsByMaterialSoftReference(
|
|||||||
const EDirectiveUtilInclusivity Inclusivity)
|
const EDirectiveUtilInclusivity Inclusivity)
|
||||||
{
|
{
|
||||||
const TArray<AActor*> SourceActors = SelectionMethod == Selection ? GetSelectedLevelActors() : GetAllLevelActors();
|
const TArray<AActor*> SourceActors = SelectionMethod == Selection ? GetSelectedLevelActors() : GetAllLevelActors();
|
||||||
|
FilterActorsByMaterial(SourceActors, FoundActors, Material, MaterialSource, Inclusivity);
|
||||||
TArray<AStaticMeshActor*> StaticMeshActors;
|
|
||||||
FilterStaticMeshActors(StaticMeshActors, SourceActors);
|
|
||||||
|
|
||||||
FilterActorsByPredicate(StaticMeshActors, FoundActors, Inclusivity, [&](AStaticMeshActor* StaticMeshActor) -> bool
|
|
||||||
{
|
|
||||||
const UStaticMeshComponent* StaticMeshComp = StaticMeshActor->GetStaticMeshComponent();
|
|
||||||
if (!StaticMeshComp) { return false; }
|
|
||||||
const UStaticMesh* Mesh = StaticMeshComp->GetStaticMesh();
|
|
||||||
if (!Mesh) { return false; }
|
|
||||||
|
|
||||||
if (MaterialSource == BaseAndOverride || MaterialSource == OverrideOnly)
|
|
||||||
{
|
|
||||||
for (int32 i = 0; i < StaticMeshComp->GetNumMaterials(); i++)
|
|
||||||
{
|
|
||||||
if (StaticMeshComp->GetMaterial(i) == Material) { return true; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (MaterialSource == BaseAndOverride || MaterialSource == BaseOnly)
|
|
||||||
{
|
|
||||||
for (int32 i = 0; i < Mesh->GetStaticMaterials().Num(); i++)
|
|
||||||
{
|
|
||||||
if (Mesh->GetMaterial(i) == Material) { return true; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
UE_LOG(LogDirectiveUtilEditor, Display, TEXT("%i actors with the material reference were found."),
|
|
||||||
FoundActors.Num());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void UDirectiveUtilEditorActorSubsystem::GetActorsByMaterialName(
|
void UDirectiveUtilEditorActorSubsystem::GetActorsByMaterialName(
|
||||||
@@ -941,38 +1308,7 @@ void UDirectiveUtilEditorActorSubsystem::GetActorsByMaterialName(
|
|||||||
const EDirectiveUtilInclusivity Inclusivity)
|
const EDirectiveUtilInclusivity Inclusivity)
|
||||||
{
|
{
|
||||||
const TArray<AActor*> SourceActors = SelectionMethod == Selection ? GetSelectedLevelActors() : GetAllLevelActors();
|
const TArray<AActor*> SourceActors = SelectionMethod == Selection ? GetSelectedLevelActors() : GetAllLevelActors();
|
||||||
|
FilterActorsByMaterialName(SourceActors, FoundActors, MaterialName, MaterialSource, Inclusivity);
|
||||||
TArray<AStaticMeshActor*> StaticMeshActors;
|
|
||||||
FilterStaticMeshActors(StaticMeshActors, SourceActors);
|
|
||||||
|
|
||||||
FilterActorsByPredicate(StaticMeshActors, FoundActors, Inclusivity, [&](AStaticMeshActor* StaticMeshActor) -> bool
|
|
||||||
{
|
|
||||||
const UStaticMeshComponent* StaticMeshComp = StaticMeshActor->GetStaticMeshComponent();
|
|
||||||
if (!StaticMeshComp) { return false; }
|
|
||||||
const UStaticMesh* Mesh = StaticMeshComp->GetStaticMesh();
|
|
||||||
if (!Mesh) { return false; }
|
|
||||||
|
|
||||||
if (MaterialSource == BaseAndOverride || MaterialSource == OverrideOnly)
|
|
||||||
{
|
|
||||||
for (int32 i = 0; i < StaticMeshComp->GetNumMaterials(); i++)
|
|
||||||
{
|
|
||||||
const UMaterialInterface* Mat = StaticMeshComp->GetMaterial(i);
|
|
||||||
if (Mat && Mat->GetName().Contains(MaterialName)) { return true; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (MaterialSource == BaseAndOverride || MaterialSource == BaseOnly)
|
|
||||||
{
|
|
||||||
for (int32 i = 0; i < Mesh->GetStaticMaterials().Num(); i++)
|
|
||||||
{
|
|
||||||
const UMaterialInterface* Mat = Mesh->GetMaterial(i);
|
|
||||||
if (Mat && Mat->GetName().Contains(MaterialName)) { return true; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
UE_LOG(LogDirectiveUtilEditor, Display, TEXT("%i actors with material %s were found."), FoundActors.Num(),
|
|
||||||
*MaterialName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void UDirectiveUtilEditorActorSubsystem::GetActorsByVertexCount(
|
void UDirectiveUtilEditorActorSubsystem::GetActorsByVertexCount(
|
||||||
@@ -1288,11 +1624,11 @@ void UDirectiveUtilEditorActorSubsystem::GetActorsByTextureSoftReference(
|
|||||||
{
|
{
|
||||||
if (const UMaterialExpressionTextureSample* TextureSample = Cast<UMaterialExpressionTextureSample>(Expression))
|
if (const UMaterialExpressionTextureSample* TextureSample = Cast<UMaterialExpressionTextureSample>(Expression))
|
||||||
{
|
{
|
||||||
if (TextureSample->Texture == Texture) { return true; }
|
if (MatchesTextureReference(TextureSample->Texture, Texture)) { return true; }
|
||||||
}
|
}
|
||||||
if (const UMaterialExpressionTextureObject* TextureObject = Cast<UMaterialExpressionTextureObject>(Expression))
|
if (const UMaterialExpressionTextureObject* TextureObject = Cast<UMaterialExpressionTextureObject>(Expression))
|
||||||
{
|
{
|
||||||
if (TextureObject->Texture == Texture) { return true; }
|
if (MatchesTextureReference(TextureObject->Texture, Texture)) { return true; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1341,8 +1677,7 @@ void UDirectiveUtilEditorActorSubsystem::GetActorsByTextureName(
|
|||||||
|
|
||||||
void UDirectiveUtilEditorActorSubsystem::GetInvalidActors(TArray<AActor*>& FoundActors)
|
void UDirectiveUtilEditorActorSubsystem::GetInvalidActors(TArray<AActor*>& FoundActors)
|
||||||
{
|
{
|
||||||
for (AActor* Actor : GetAllLevelActors()) { if (!IsValid(Actor)) { FoundActors.AddUnique(Actor); } }
|
FoundActors.Reset();
|
||||||
UE_LOG(LogDirectiveUtilEditor, Display, TEXT("%i invalid actors were found."), FoundActors.Num());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void UDirectiveUtilEditorActorSubsystem::PushOverrideMaterialsToSource(UStaticMeshComponent* StaticMeshComponent)
|
void UDirectiveUtilEditorActorSubsystem::PushOverrideMaterialsToSource(UStaticMeshComponent* StaticMeshComponent)
|
||||||
@@ -1361,11 +1696,14 @@ void UDirectiveUtilEditorActorSubsystem::PushOverrideMaterialsToSource(UStaticMe
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FScopedTransaction Transaction(NSLOCTEXT("DirectiveUtilities", "PushOverrideMaterialsToSource", "Push Override Materials To Source"));
|
const FScopedTransaction Transaction(NSLOCTEXT("DirectiveUtilities", "PushOverrideMaterialsToSource", "Push Override Materials To Source"));
|
||||||
for (int32 i = 0; i < StaticMeshComponent->GetNumMaterials(); i++)
|
const TArray<TObjectPtr<UMaterialInterface>>& OverrideMaterials = StaticMeshComponent->OverrideMaterials;
|
||||||
|
const int32 MaterialSlotCount = StaticMesh->GetStaticMaterials().Num();
|
||||||
|
for (int32 MaterialIndex = 0; MaterialIndex < FMath::Min(OverrideMaterials.Num(), MaterialSlotCount); ++MaterialIndex)
|
||||||
{
|
{
|
||||||
if (UMaterialInterface* Material = StaticMeshComponent->GetMaterial(i))
|
UMaterialInterface* OverrideMaterial = OverrideMaterials[MaterialIndex];
|
||||||
|
if (OverrideMaterial && StaticMesh->GetMaterial(MaterialIndex) != OverrideMaterial)
|
||||||
{
|
{
|
||||||
StaticMesh->SetMaterial(i, Material);
|
StaticMesh->SetMaterial(MaterialIndex, OverrideMaterial);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UE_LOG(LogDirectiveUtilEditor, Display, TEXT("Materials were pushed to source for %s."), *StaticMeshComponent->GetName());
|
UE_LOG(LogDirectiveUtilEditor, Display, TEXT("Materials were pushed to source for %s."), *StaticMeshComponent->GetName());
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "Tasks/DirectiveUtilEditorSlowTask.h"
|
||||||
|
|
||||||
|
#include "Misc/ScopedSlowTask.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
UDirectiveUtilEditorSlowTask* ActiveSlowTask = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilEditorSlowTask::Initialize(
|
||||||
|
const float TotalWork,
|
||||||
|
const FText& Description,
|
||||||
|
const bool bCanCancel,
|
||||||
|
const bool bShowDialog)
|
||||||
|
{
|
||||||
|
Finish();
|
||||||
|
if (!FMath::IsFinite(TotalWork) || TotalWork <= 0.0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ActiveSlowTask && ActiveSlowTask != this)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SlowTask = new FScopedSlowTask(TotalWork, Description);
|
||||||
|
ActiveSlowTask = this;
|
||||||
|
AddToRoot();
|
||||||
|
if (bShowDialog)
|
||||||
|
{
|
||||||
|
SlowTask->MakeDialog(bCanCancel, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDirectiveUtilEditorSlowTask::Advance(const float Work, const FText& Message)
|
||||||
|
{
|
||||||
|
if (!SlowTask || !FMath::IsFinite(Work) || Work < 0.0f)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float CompletedWork = SlowTask->CompletedWork + SlowTask->CurrentFrameScope;
|
||||||
|
const float RemainingWork = FMath::Max(0.0f, SlowTask->TotalAmountOfWork - CompletedWork);
|
||||||
|
SlowTask->EnterProgressFrame(FMath::Min(Work, RemainingWork), Message);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDirectiveUtilEditorSlowTask::IsCancelRequested() const
|
||||||
|
{
|
||||||
|
return SlowTask && SlowTask->ShouldCancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDirectiveUtilEditorSlowTask::IsActive() const
|
||||||
|
{
|
||||||
|
return SlowTask != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilEditorSlowTask::Finish()
|
||||||
|
{
|
||||||
|
if (!SlowTask)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete SlowTask;
|
||||||
|
SlowTask = nullptr;
|
||||||
|
if (ActiveSlowTask == this)
|
||||||
|
{
|
||||||
|
ActiveSlowTask = nullptr;
|
||||||
|
}
|
||||||
|
if (IsRooted())
|
||||||
|
{
|
||||||
|
RemoveFromRoot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilEditorSlowTask::FinishActiveTask()
|
||||||
|
{
|
||||||
|
if (ActiveSlowTask)
|
||||||
|
{
|
||||||
|
ActiveSlowTask->Finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||||
|
#include "Types/DirectiveUtilEditorAuditTypes.h"
|
||||||
|
#include "DirectiveUtilEditorAssetAuditLibrary.generated.h"
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class DIRECTIVEUTILITIESEDITOR_API UDirectiveUtilEditorAssetAuditLibrary : public UBlueprintFunctionLibrary
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
/** Returns assets with no known Asset Registry referencers. Results are candidates and may still be loaded indirectly. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Asset Audit")
|
||||||
|
static TArray<FAssetData> FindUnreferencedAssetCandidates(const FDirectiveUtilAssetAuditOptions& Options);
|
||||||
|
|
||||||
|
/** Returns Asset Registry dependencies whose packages cannot be found. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Asset Audit")
|
||||||
|
static TArray<FDirectiveUtilMissingAssetReference> FindMissingAssetReferences(const FDirectiveUtilAssetAuditOptions& Options);
|
||||||
|
|
||||||
|
/** Returns dependency cycles between assets in the scanned paths. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Asset Audit")
|
||||||
|
static TArray<FDirectiveUtilAssetDependencyCycle> FindAssetDependencyCycles(const FDirectiveUtilAssetAuditOptions& Options);
|
||||||
|
|
||||||
|
/** Builds a read-only report for assets in the scanned paths. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Asset Audit")
|
||||||
|
static FDirectiveUtilAssetAuditReport BuildAssetAuditReport(const FDirectiveUtilAssetAuditOptions& Options);
|
||||||
|
|
||||||
|
/** Converts an asset audit report to CSV text. */
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Editor|Asset Audit")
|
||||||
|
static FString AssetAuditReportToCsv(const FDirectiveUtilAssetAuditReport& Report);
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "AssetRegistry/AssetData.h"
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "EditorAssetLibrary.h"
|
#include "EditorAssetLibrary.h"
|
||||||
#include "Types/DirectiveUtilEditorAssetTypes.h"
|
#include "Types/DirectiveUtilEditorAssetTypes.h"
|
||||||
@@ -44,7 +45,8 @@ public:
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Fixes up (and deletes) object redirectors found under the given directories, without loading every asset.
|
* Fixes up (and deletes) object redirectors found under the given directories, without loading every asset.
|
||||||
* Equivalent to the Content Browser's "Fix Up Redirectors in Folder", but scriptable and headless-friendly.
|
* Equivalent to the Content Browser's "Fix Up Redirectors in Folder". Unreal shows a completion dialog,
|
||||||
|
* so this returns Failure in unattended sessions instead of blocking the process.
|
||||||
* @param DirectoryPaths Directories to scan for redirectors. If empty, the entire registry is scanned.
|
* @param 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).
|
* @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).
|
* @return Success if the operation ran (even if nothing needed fixing), Failure otherwise (e.g. a fixup is already in progress).
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "AssetRegistry/AssetData.h"
|
||||||
|
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||||
|
#include "Types/DirectiveUtilEditorBlueprintTypes.h"
|
||||||
|
#include "DirectiveUtilEditorBlueprintLibrary.generated.h"
|
||||||
|
|
||||||
|
class UBlueprint;
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class DIRECTIVEUTILITIESEDITOR_API UDirectiveUtilEditorBlueprintLibrary : public UBlueprintFunctionLibrary
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Editor|Blueprint Inspection")
|
||||||
|
static EDirectiveUtilBlueprintCompileStatus GetBlueprintCompileStatus(const UBlueprint* Blueprint);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Blueprint Inspection")
|
||||||
|
static TArray<FAssetData> FindBlueprintsByCompileStatus(
|
||||||
|
EDirectiveUtilBlueprintCompileStatus CompileStatus,
|
||||||
|
const FDirectiveUtilBlueprintSearchOptions& Options);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Blueprint Inspection")
|
||||||
|
static TArray<FAssetData> FindBlueprintsByParentClass(
|
||||||
|
UClass* ParentClass,
|
||||||
|
const FDirectiveUtilBlueprintSearchOptions& Options,
|
||||||
|
bool bIncludeDescendants = true);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Blueprint Inspection")
|
||||||
|
static TArray<FAssetData> FindBlueprintsImplementingInterface(
|
||||||
|
UClass* InterfaceClass,
|
||||||
|
const FDirectiveUtilBlueprintSearchOptions& Options);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Blueprint Inspection")
|
||||||
|
static TArray<FAssetData> FindBlueprintsContainingComponentClass(
|
||||||
|
UClass* ComponentClass,
|
||||||
|
const FDirectiveUtilBlueprintSearchOptions& Options,
|
||||||
|
bool bIncludeDerivedComponents = true);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Editor|Blueprint Inspection")
|
||||||
|
static TArray<FName> GetUnusedBlueprintVariables(
|
||||||
|
UBlueprint* Blueprint,
|
||||||
|
bool bIncludeExternallyAccessibleVariables = false);
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||||
|
#include "Types/DirectiveUtilEditorTaskTypes.h"
|
||||||
|
#include "DirectiveUtilEditorTaskLibrary.generated.h"
|
||||||
|
|
||||||
|
class UDirectiveUtilEditorSlowTask;
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class DIRECTIVEUTILITIESEDITOR_API UDirectiveUtilEditorTaskLibrary : public UBlueprintFunctionLibrary
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
/** Starts a modal editor progress task. TotalWork must be greater than zero, and only one task can be active. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Task")
|
||||||
|
static UDirectiveUtilEditorSlowTask* StartEditorSlowTask(float TotalWork, const FText& Description, bool bCanCancel = false);
|
||||||
|
|
||||||
|
/** Displays a fire-and-forget editor notification. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Notification")
|
||||||
|
static bool ShowEditorNotification(const FText& Message, EDirectiveUtilEditorNotificationState State = EDirectiveUtilEditorNotificationState::Neutral, float ExpireDuration = 3.0f);
|
||||||
|
};
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
class UCapsuleComponent;
|
class UCapsuleComponent;
|
||||||
class UBoxComponent;
|
class UBoxComponent;
|
||||||
class USphereComponent;
|
class USphereComponent;
|
||||||
class UStaticMeshActor;
|
class AStaticMeshActor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DirectiveUtilEditorActorSubsystem
|
* DirectiveUtilEditorActorSubsystem
|
||||||
@@ -44,6 +44,30 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor")
|
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor")
|
||||||
TArray<UClass*> GetAllLevelClasses();
|
TArray<UClass*> GetAllLevelClasses();
|
||||||
|
|
||||||
|
/** Aligns actor bounds to the combined minimum, center, or maximum on one axis. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Actor Layout")
|
||||||
|
static FDirectiveUtilActorOperationResult AlignActors(
|
||||||
|
const TArray<AActor*>& Actors,
|
||||||
|
EDirectiveUtilActorLayoutAxis Axis,
|
||||||
|
EDirectiveUtilActorAlignment Alignment);
|
||||||
|
|
||||||
|
/** Distributes actors between the outer actors using equal center spacing or equal bounds gaps. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Actor Layout")
|
||||||
|
static FDirectiveUtilActorOperationResult DistributeActors(
|
||||||
|
const TArray<AActor*>& Actors,
|
||||||
|
EDirectiveUtilActorLayoutAxis Axis,
|
||||||
|
EDirectiveUtilActorDistribution Distribution);
|
||||||
|
|
||||||
|
/** Traces from each actor and places its pivot or directional bounds on the hit surface. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Actor Layout", meta = (AdvancedDisplay = "bAlignToNormal"))
|
||||||
|
static FDirectiveUtilActorOperationResult SnapActorsToSurface(
|
||||||
|
const TArray<AActor*>& Actors,
|
||||||
|
FVector TraceDirection,
|
||||||
|
float MaximumDistance,
|
||||||
|
TEnumAsByte<ECollisionChannel> TraceChannel = ECC_Visibility,
|
||||||
|
EDirectiveUtilSurfacePlacement Placement = EDirectiveUtilSurfacePlacement::Bounds,
|
||||||
|
bool bAlignToNormal = false);
|
||||||
|
|
||||||
//-----------------------------
|
//-----------------------------
|
||||||
// Filters
|
// Filters
|
||||||
//-----------------------------
|
//-----------------------------
|
||||||
@@ -679,7 +703,7 @@ public:
|
|||||||
* Returns a list of invalid actors.
|
* Returns a list of invalid actors.
|
||||||
* @param FoundActors The list of actors that were found.
|
* @param FoundActors The list of actors that were found.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(AdvancedDisplay=1))
|
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(DeprecatedFunction, DeprecationMessage="Actor enumeration excludes invalid actors. This node always returns an empty array."))
|
||||||
void GetInvalidActors(TArray<AActor*>& FoundActors);
|
void GetInvalidActors(TArray<AActor*>& FoundActors);
|
||||||
|
|
||||||
//-----------------------------
|
//-----------------------------
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "DirectiveUtilEditorSlowTask.generated.h"
|
||||||
|
|
||||||
|
struct FScopedSlowTask;
|
||||||
|
|
||||||
|
UCLASS(BlueprintType)
|
||||||
|
class DIRECTIVEUTILITIESEDITOR_API UDirectiveUtilEditorSlowTask : public UObject
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Task")
|
||||||
|
bool Advance(float Work, const FText& Message);
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Editor|Task")
|
||||||
|
bool IsCancelRequested() const;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Editor|Task")
|
||||||
|
bool IsActive() const;
|
||||||
|
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Task")
|
||||||
|
void Finish();
|
||||||
|
|
||||||
|
void Initialize(float TotalWork, const FText& Description, bool bCanCancel, bool bShowDialog);
|
||||||
|
static void FinishActiveTask();
|
||||||
|
|
||||||
|
private:
|
||||||
|
FScopedSlowTask* SlowTask = nullptr;
|
||||||
|
};
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "AssetRegistry/AssetData.h"
|
||||||
|
#include "DirectiveUtilEditorAuditTypes.generated.h"
|
||||||
|
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EDirectiveUtilAssetDependencyType : uint8
|
||||||
|
{
|
||||||
|
HardPackage UMETA(DisplayName = "Hard Package"),
|
||||||
|
SoftPackage UMETA(DisplayName = "Soft Package"),
|
||||||
|
SearchableName UMETA(DisplayName = "Searchable Name"),
|
||||||
|
DirectManagement UMETA(DisplayName = "Direct Management"),
|
||||||
|
IndirectManagement UMETA(DisplayName = "Indirect Management"),
|
||||||
|
};
|
||||||
|
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FDirectiveUtilAssetAuditOptions
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Directive Utilities|Asset Audit")
|
||||||
|
TArray<FName> PackagePaths;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Directive Utilities|Asset Audit")
|
||||||
|
TArray<FName> ExcludedPackagePaths;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Directive Utilities|Asset Audit")
|
||||||
|
bool bUseDefaultPathExclusions = true;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Directive Utilities|Asset Audit")
|
||||||
|
bool bIncludePrimaryAssets = false;
|
||||||
|
|
||||||
|
FDirectiveUtilAssetAuditOptions()
|
||||||
|
{
|
||||||
|
PackagePaths.Add(TEXT("/Game"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FDirectiveUtilMissingAssetReference
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
|
||||||
|
FAssetData ReferencingAsset;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
|
||||||
|
FName MissingPackage;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
|
||||||
|
EDirectiveUtilAssetDependencyType DependencyType = EDirectiveUtilAssetDependencyType::SoftPackage;
|
||||||
|
};
|
||||||
|
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FDirectiveUtilAssetDependencyCycle
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
|
||||||
|
TArray<FName> Packages;
|
||||||
|
};
|
||||||
|
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FDirectiveUtilAssetAuditEntry
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
|
||||||
|
FAssetData Asset;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
|
||||||
|
FName PackageName;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
|
||||||
|
FName PackagePath;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
|
||||||
|
FString AssetClass;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
|
||||||
|
int64 DiskSize = 0;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
|
||||||
|
int32 DependencyCount = 0;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
|
||||||
|
int32 ReferencerCount = 0;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
|
||||||
|
bool bPrimaryAsset = false;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
|
||||||
|
TArray<FString> Findings;
|
||||||
|
};
|
||||||
|
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FDirectiveUtilAssetAuditReport
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
|
||||||
|
TArray<FDirectiveUtilAssetAuditEntry> Assets;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
|
||||||
|
TArray<FDirectiveUtilMissingAssetReference> MissingReferences;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Asset Audit")
|
||||||
|
TArray<FDirectiveUtilAssetDependencyCycle> DependencyCycles;
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "DirectiveUtilEditorBlueprintTypes.generated.h"
|
||||||
|
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EDirectiveUtilBlueprintCompileStatus : uint8
|
||||||
|
{
|
||||||
|
Unknown,
|
||||||
|
Dirty,
|
||||||
|
Error,
|
||||||
|
UpToDate UMETA(DisplayName = "Up To Date"),
|
||||||
|
BeingCreated UMETA(DisplayName = "Being Created"),
|
||||||
|
UpToDateWithWarnings UMETA(DisplayName = "Up To Date With Warnings"),
|
||||||
|
};
|
||||||
|
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FDirectiveUtilBlueprintSearchOptions
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Directive Utilities|Blueprint Inspection")
|
||||||
|
TArray<FName> PackagePaths;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Directive Utilities|Blueprint Inspection")
|
||||||
|
TArray<FName> ExcludedPackagePaths;
|
||||||
|
|
||||||
|
FDirectiveUtilBlueprintSearchOptions()
|
||||||
|
{
|
||||||
|
PackagePaths.Add(TEXT("/Game"));
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "DirectiveUtilEditorTaskTypes.generated.h"
|
||||||
|
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EDirectiveUtilEditorNotificationState : uint8
|
||||||
|
{
|
||||||
|
Neutral,
|
||||||
|
Success,
|
||||||
|
Warning,
|
||||||
|
Failure
|
||||||
|
};
|
||||||
@@ -3,6 +3,51 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
|
#include "DirectiveUtilEditorTypes.generated.h"
|
||||||
|
|
||||||
|
class AActor;
|
||||||
|
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EDirectiveUtilActorLayoutAxis : uint8
|
||||||
|
{
|
||||||
|
X,
|
||||||
|
Y,
|
||||||
|
Z,
|
||||||
|
};
|
||||||
|
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EDirectiveUtilActorAlignment : uint8
|
||||||
|
{
|
||||||
|
Minimum,
|
||||||
|
Center,
|
||||||
|
Maximum,
|
||||||
|
};
|
||||||
|
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EDirectiveUtilActorDistribution : uint8
|
||||||
|
{
|
||||||
|
Centers,
|
||||||
|
BoundsGaps UMETA(DisplayName = "Bounds Gaps"),
|
||||||
|
};
|
||||||
|
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EDirectiveUtilSurfacePlacement : uint8
|
||||||
|
{
|
||||||
|
Pivot,
|
||||||
|
Bounds,
|
||||||
|
};
|
||||||
|
|
||||||
|
USTRUCT(BlueprintType)
|
||||||
|
struct FDirectiveUtilActorOperationResult
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Actor Layout")
|
||||||
|
TArray<TObjectPtr<AActor>> ChangedActors;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintReadOnly, Category = "Directive Utilities|Actor Layout")
|
||||||
|
TArray<TObjectPtr<AActor>> SkippedActors;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EDirectiveUtilSelectionMethod
|
* EDirectiveUtilSelectionMethod
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ public class DirectiveUtilitiesRuntime : ModuleRules
|
|||||||
new string[]
|
new string[]
|
||||||
{
|
{
|
||||||
"Core",
|
"Core",
|
||||||
|
"CoreUObject",
|
||||||
|
"Engine",
|
||||||
"GameplayTags",
|
"GameplayTags",
|
||||||
"NetCore",
|
"NetCore",
|
||||||
}
|
}
|
||||||
@@ -22,11 +24,10 @@ public class DirectiveUtilitiesRuntime : ModuleRules
|
|||||||
PrivateDependencyModuleNames.AddRange(
|
PrivateDependencyModuleNames.AddRange(
|
||||||
new string[]
|
new string[]
|
||||||
{
|
{
|
||||||
"CoreUObject",
|
|
||||||
"Engine",
|
|
||||||
"Slate",
|
"Slate",
|
||||||
"SlateCore",
|
"SlateCore",
|
||||||
"AIModule",
|
"AIModule",
|
||||||
|
"NavigationSystem",
|
||||||
"EnhancedInput",
|
"EnhancedInput",
|
||||||
"ApplicationCore",
|
"ApplicationCore",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#include "DirectiveUtilLogChannels.h"
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "DirectiveUtilLogChannels.h"
|
||||||
|
|
||||||
DEFINE_LOG_CATEGORY(LogDirectiveUtil);
|
DEFINE_LOG_CATEGORY(LogDirectiveUtil);
|
||||||
DEFINE_LOG_CATEGORY(LogDirectiveUtilEditor);
|
DEFINE_LOG_CATEGORY(LogDirectiveUtilEditor);
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -2,9 +2,92 @@
|
|||||||
|
|
||||||
|
|
||||||
#include "Libraries/DirectiveUtilFunctionLibrary.h"
|
#include "Libraries/DirectiveUtilFunctionLibrary.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "HAL/CriticalSection.h"
|
||||||
#include "HAL/PlatformApplicationMisc.h"
|
#include "HAL/PlatformApplicationMisc.h"
|
||||||
|
#include "HAL/PlatformTime.h"
|
||||||
|
#include "Misc/App.h"
|
||||||
#include "Misc/CommandLine.h"
|
#include "Misc/CommandLine.h"
|
||||||
#include "Misc/ConfigCacheIni.h"
|
#include "Misc/ConfigCacheIni.h"
|
||||||
|
#include "Misc/ScopeLock.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
struct FDirectiveUtilStopwatchState
|
||||||
|
{
|
||||||
|
FCriticalSection Lock;
|
||||||
|
TMap<FName, double> StartTimesByKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
FDirectiveUtilStopwatchState StopwatchState;
|
||||||
|
|
||||||
|
EDirectiveUtilWorldType ToDirectiveWorldType(const EWorldType::Type WorldType)
|
||||||
|
{
|
||||||
|
switch (WorldType)
|
||||||
|
{
|
||||||
|
case EWorldType::None:
|
||||||
|
return EDirectiveUtilWorldType::None;
|
||||||
|
case EWorldType::Game:
|
||||||
|
return EDirectiveUtilWorldType::Game;
|
||||||
|
case EWorldType::Editor:
|
||||||
|
return EDirectiveUtilWorldType::Editor;
|
||||||
|
case EWorldType::PIE:
|
||||||
|
return EDirectiveUtilWorldType::PlayInEditor;
|
||||||
|
case EWorldType::EditorPreview:
|
||||||
|
return EDirectiveUtilWorldType::EditorPreview;
|
||||||
|
case EWorldType::GamePreview:
|
||||||
|
return EDirectiveUtilWorldType::GamePreview;
|
||||||
|
case EWorldType::GameRPC:
|
||||||
|
return EDirectiveUtilWorldType::GameRPC;
|
||||||
|
case EWorldType::Inactive:
|
||||||
|
return EDirectiveUtilWorldType::Inactive;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EDirectiveUtilWorldType::Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
EDirectiveUtilBuildConfiguration ToDirectiveBuildConfiguration(const EBuildConfiguration BuildConfiguration)
|
||||||
|
{
|
||||||
|
switch (BuildConfiguration)
|
||||||
|
{
|
||||||
|
case EBuildConfiguration::Unknown:
|
||||||
|
return EDirectiveUtilBuildConfiguration::Unknown;
|
||||||
|
case EBuildConfiguration::Debug:
|
||||||
|
return EDirectiveUtilBuildConfiguration::Debug;
|
||||||
|
case EBuildConfiguration::DebugGame:
|
||||||
|
return EDirectiveUtilBuildConfiguration::DebugGame;
|
||||||
|
case EBuildConfiguration::Development:
|
||||||
|
return EDirectiveUtilBuildConfiguration::Development;
|
||||||
|
case EBuildConfiguration::Shipping:
|
||||||
|
return EDirectiveUtilBuildConfiguration::Shipping;
|
||||||
|
case EBuildConfiguration::Test:
|
||||||
|
return EDirectiveUtilBuildConfiguration::Test;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EDirectiveUtilBuildConfiguration::Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
EDirectiveUtilBuildTargetType ToDirectiveBuildTargetType(const EBuildTargetType BuildTargetType)
|
||||||
|
{
|
||||||
|
switch (BuildTargetType)
|
||||||
|
{
|
||||||
|
case EBuildTargetType::Unknown:
|
||||||
|
return EDirectiveUtilBuildTargetType::Unknown;
|
||||||
|
case EBuildTargetType::Game:
|
||||||
|
return EDirectiveUtilBuildTargetType::Game;
|
||||||
|
case EBuildTargetType::Server:
|
||||||
|
return EDirectiveUtilBuildTargetType::Server;
|
||||||
|
case EBuildTargetType::Client:
|
||||||
|
return EDirectiveUtilBuildTargetType::Client;
|
||||||
|
case EBuildTargetType::Editor:
|
||||||
|
return EDirectiveUtilBuildTargetType::Editor;
|
||||||
|
case EBuildTargetType::Program:
|
||||||
|
return EDirectiveUtilBuildTargetType::Program;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EDirectiveUtilBuildTargetType::Unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void UDirectiveUtilFunctionLibrary::GetChildClasses(const UClass* BaseClass, const bool bRecursive, TArray<UClass*>& DerivedClasses)
|
void UDirectiveUtilFunctionLibrary::GetChildClasses(const UClass* BaseClass, const bool bRecursive, TArray<UClass*>& DerivedClasses)
|
||||||
{
|
{
|
||||||
@@ -57,6 +140,22 @@ bool UDirectiveUtilFunctionLibrary::IsRunningInEditor()
|
|||||||
return GIsEditor;
|
return GIsEditor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EDirectiveUtilWorldType UDirectiveUtilFunctionLibrary::GetWorldType(const UObject* WorldContextObject)
|
||||||
|
{
|
||||||
|
const UWorld* World = WorldContextObject ? WorldContextObject->GetWorld() : nullptr;
|
||||||
|
return World ? ToDirectiveWorldType(World->WorldType) : EDirectiveUtilWorldType::Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
EDirectiveUtilBuildConfiguration UDirectiveUtilFunctionLibrary::GetBuildConfigurationType()
|
||||||
|
{
|
||||||
|
return ToDirectiveBuildConfiguration(FApp::GetBuildConfiguration());
|
||||||
|
}
|
||||||
|
|
||||||
|
EDirectiveUtilBuildTargetType UDirectiveUtilFunctionLibrary::GetBuildTargetType()
|
||||||
|
{
|
||||||
|
return ToDirectiveBuildTargetType(FApp::GetBuildTargetType());
|
||||||
|
}
|
||||||
|
|
||||||
bool UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(const FString& Switch)
|
bool UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(const FString& Switch)
|
||||||
{
|
{
|
||||||
return HasCommandLineSwitch(FCommandLine::Get(), Switch);
|
return HasCommandLineSwitch(FCommandLine::Get(), Switch);
|
||||||
@@ -67,6 +166,44 @@ bool UDirectiveUtilFunctionLibrary::GetCommandLineOption(const FString& Key, FSt
|
|||||||
return GetCommandLineOption(FCommandLine::Get(), Key, OutValue);
|
return GetCommandLineOption(FCommandLine::Get(), Key, OutValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool UDirectiveUtilFunctionLibrary::StartStopwatch(const FName Key, const bool bRestartIfRunning)
|
||||||
|
{
|
||||||
|
if (Key.IsNone())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FScopeLock Lock(&StopwatchState.Lock);
|
||||||
|
if (!bRestartIfRunning && StopwatchState.StartTimesByKey.Contains(Key))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
StopwatchState.StartTimesByKey.Add(Key, FPlatformTime::Seconds());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDirectiveUtilFunctionLibrary::StopStopwatch(const FName Key, double& ElapsedMilliseconds)
|
||||||
|
{
|
||||||
|
ElapsedMilliseconds = 0.0;
|
||||||
|
if (Key.IsNone())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
double StartTime = 0.0;
|
||||||
|
{
|
||||||
|
FScopeLock Lock(&StopwatchState.Lock);
|
||||||
|
if (!StopwatchState.StartTimesByKey.RemoveAndCopyValue(Key, StartTime))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ElapsedMilliseconds = FMath::Max((FPlatformTime::Seconds() - StartTime) * 1000.0, 0.0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(const TCHAR* CommandLine, const FString& Switch)
|
bool UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(const TCHAR* CommandLine, const FString& Switch)
|
||||||
{
|
{
|
||||||
if (Switch.IsEmpty())
|
if (Switch.IsEmpty())
|
||||||
|
|||||||
@@ -27,13 +27,26 @@ FGameplayTagContainer UDirectiveUtilGameplayTagFunctionLibrary::GetTagParents(co
|
|||||||
|
|
||||||
int32 UDirectiveUtilGameplayTagFunctionLibrary::GetTagDepth(const FGameplayTag& Tag)
|
int32 UDirectiveUtilGameplayTagFunctionLibrary::GetTagDepth(const FGameplayTag& Tag)
|
||||||
{
|
{
|
||||||
return GetTagSegments(Tag).Num();
|
int32 Depth = 0;
|
||||||
|
for (FGameplayTag Current = Tag; Current.IsValid(); Current = Current.RequestDirectParent())
|
||||||
|
{
|
||||||
|
++Depth;
|
||||||
|
}
|
||||||
|
return Depth;
|
||||||
}
|
}
|
||||||
|
|
||||||
FString UDirectiveUtilGameplayTagFunctionLibrary::GetTagLeafName(const FGameplayTag& Tag)
|
FString UDirectiveUtilGameplayTagFunctionLibrary::GetTagLeafName(const FGameplayTag& Tag)
|
||||||
{
|
{
|
||||||
const TArray<FString> Segments = GetTagSegments(Tag);
|
if (!Tag.IsValid())
|
||||||
return Segments.Num() > 0 ? Segments.Last() : FString();
|
{
|
||||||
|
return FString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const FString TagString = Tag.GetTagName().ToString();
|
||||||
|
int32 SeparatorIndex = INDEX_NONE;
|
||||||
|
return TagString.FindLastChar(TEXT('.'), SeparatorIndex)
|
||||||
|
? TagString.Mid(SeparatorIndex + 1)
|
||||||
|
: TagString;
|
||||||
}
|
}
|
||||||
|
|
||||||
TArray<FString> UDirectiveUtilGameplayTagFunctionLibrary::GetTagSegments(const FGameplayTag& Tag)
|
TArray<FString> UDirectiveUtilGameplayTagFunctionLibrary::GetTagSegments(const FGameplayTag& Tag)
|
||||||
@@ -64,10 +77,9 @@ FGameplayTagContainer UDirectiveUtilGameplayTagFunctionLibrary::GetTagDirectChil
|
|||||||
return DirectChildren;
|
return DirectChildren;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int32 DirectChildDepth = GetTagDepth(Tag) + 1;
|
|
||||||
for (const FGameplayTag& Child : GetTagChildren(Tag))
|
for (const FGameplayTag& Child : GetTagChildren(Tag))
|
||||||
{
|
{
|
||||||
if (GetTagDepth(Child) == DirectChildDepth)
|
if (Child.RequestDirectParent() == Tag)
|
||||||
{
|
{
|
||||||
DirectChildren.AddTag(Child);
|
DirectChildren.AddTag(Child);
|
||||||
}
|
}
|
||||||
@@ -77,29 +89,31 @@ FGameplayTagContainer UDirectiveUtilGameplayTagFunctionLibrary::GetTagDirectChil
|
|||||||
|
|
||||||
FGameplayTag UDirectiveUtilGameplayTagFunctionLibrary::GetTagCommonAncestor(const FGameplayTag& TagA, const FGameplayTag& TagB)
|
FGameplayTag UDirectiveUtilGameplayTagFunctionLibrary::GetTagCommonAncestor(const FGameplayTag& TagA, const FGameplayTag& TagB)
|
||||||
{
|
{
|
||||||
const TArray<FString> SegmentsA = GetTagSegments(TagA);
|
if (!TagA.IsValid() || !TagB.IsValid())
|
||||||
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();
|
return FGameplayTag();
|
||||||
}
|
}
|
||||||
// A common prefix of two registered tags is itself registered (parents auto-register).
|
|
||||||
return FGameplayTag::RequestGameplayTag(FName(*Prefix), false);
|
FGameplayTag AncestorA = TagA;
|
||||||
|
FGameplayTag AncestorB = TagB;
|
||||||
|
int32 DepthA = GetTagDepth(AncestorA);
|
||||||
|
int32 DepthB = GetTagDepth(AncestorB);
|
||||||
|
while (DepthA > DepthB)
|
||||||
|
{
|
||||||
|
AncestorA = AncestorA.RequestDirectParent();
|
||||||
|
--DepthA;
|
||||||
|
}
|
||||||
|
while (DepthB > DepthA)
|
||||||
|
{
|
||||||
|
AncestorB = AncestorB.RequestDirectParent();
|
||||||
|
--DepthB;
|
||||||
|
}
|
||||||
|
while (AncestorA.IsValid() && AncestorB.IsValid() && AncestorA != AncestorB)
|
||||||
|
{
|
||||||
|
AncestorA = AncestorA.RequestDirectParent();
|
||||||
|
AncestorB = AncestorB.RequestDirectParent();
|
||||||
|
}
|
||||||
|
return AncestorA == AncestorB ? AncestorA : FGameplayTag();
|
||||||
}
|
}
|
||||||
|
|
||||||
FGameplayTag UDirectiveUtilGameplayTagFunctionLibrary::GetTagAtDepth(const FGameplayTag& Tag, const int32 Depth)
|
FGameplayTag UDirectiveUtilGameplayTagFunctionLibrary::GetTagAtDepth(const FGameplayTag& Tag, const int32 Depth)
|
||||||
@@ -109,20 +123,18 @@ FGameplayTag UDirectiveUtilGameplayTagFunctionLibrary::GetTagAtDepth(const FGame
|
|||||||
return FGameplayTag();
|
return FGameplayTag();
|
||||||
}
|
}
|
||||||
|
|
||||||
const TArray<FString> Segments = GetTagSegments(Tag);
|
const int32 TagDepth = GetTagDepth(Tag);
|
||||||
if (Depth >= Segments.Num())
|
if (Depth >= TagDepth)
|
||||||
{
|
{
|
||||||
return Tag;
|
return Tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
FString Prefix = Segments[0];
|
FGameplayTag Ancestor = Tag;
|
||||||
for (int32 Index = 1; Index < Depth; ++Index)
|
for (int32 CurrentDepth = TagDepth; CurrentDepth > Depth; --CurrentDepth)
|
||||||
{
|
{
|
||||||
Prefix += TEXT(".");
|
Ancestor = Ancestor.RequestDirectParent();
|
||||||
Prefix += Segments[Index];
|
|
||||||
}
|
}
|
||||||
// An ancestor of a registered tag is always registered itself (parents auto-register).
|
return Ancestor;
|
||||||
return FGameplayTag::RequestGameplayTag(FName(*Prefix), false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FGameplayTagContainer UDirectiveUtilGameplayTagFunctionLibrary::GetTagSiblings(const FGameplayTag& Tag)
|
FGameplayTagContainer UDirectiveUtilGameplayTagFunctionLibrary::GetTagSiblings(const FGameplayTag& Tag)
|
||||||
|
|||||||
@@ -77,8 +77,11 @@ EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::AddInputMappingC
|
|||||||
|
|
||||||
if (FailedIndices.Num() > 0)
|
if (FailedIndices.Num() > 0)
|
||||||
{
|
{
|
||||||
FString FailedIndicesStr = FString::JoinBy(FailedIndices, TEXT(", "), [](const int32 Index) { return FString::Printf(TEXT("%d"), Index); });
|
const FString FailedIndicesString = FString::JoinBy(
|
||||||
UE_LOGFMT(LogDirectiveUtil, Warning, "{FailedIndicies} Input Mapping Contexts failed to load and were not added! The failed indexes are [{FailedIndicieIndexes}]", FailedIndices.Num(), FailedIndicesStr);
|
FailedIndices, TEXT(", "), [](const int32 Index) { return FString::FromInt(Index); });
|
||||||
|
UE_LOGFMT(LogDirectiveUtil, Warning,
|
||||||
|
"{FailedContextCount} input mapping contexts could not be loaded. Indexes: [{FailedContextIndexes}]",
|
||||||
|
FailedIndices.Num(), FailedIndicesString);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (LoadedContexts.IsEmpty())
|
if (LoadedContexts.IsEmpty())
|
||||||
@@ -104,7 +107,7 @@ EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::RemoveInputMappi
|
|||||||
AController* PlayerController,
|
AController* PlayerController,
|
||||||
const TArray<TSoftObjectPtr<UInputMappingContext>>& Contexts)
|
const TArray<TSoftObjectPtr<UInputMappingContext>>& Contexts)
|
||||||
{
|
{
|
||||||
if (Contexts.IsEmpty()) { return EDirectiveUtilSuccessStatus::Failure;; }
|
if (Contexts.IsEmpty()) { return EDirectiveUtilSuccessStatus::Failure; }
|
||||||
|
|
||||||
UEnhancedInputLocalPlayerSubsystem* EnhancedInput;
|
UEnhancedInputLocalPlayerSubsystem* EnhancedInput;
|
||||||
const bool bEnhancedInputRetrievedFromController = TryGetEnhancedInputSubsystemFromController(PlayerController, EnhancedInput);
|
const bool bEnhancedInputRetrievedFromController = TryGetEnhancedInputSubsystemFromController(PlayerController, EnhancedInput);
|
||||||
@@ -118,10 +121,11 @@ EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::RemoveInputMappi
|
|||||||
for (int32 Index = 0; Index < Contexts.Num(); ++Index)
|
for (int32 Index = 0; Index < Contexts.Num(); ++Index)
|
||||||
{
|
{
|
||||||
const TSoftObjectPtr<UInputMappingContext>& Context = Contexts[Index];
|
const TSoftObjectPtr<UInputMappingContext>& Context = Contexts[Index];
|
||||||
if (const UInputMappingContext* MappingContext = Context.LoadSynchronous())
|
if (const UInputMappingContext* MappingContext = Context.Get())
|
||||||
{
|
{
|
||||||
EnhancedInput->RemoveMappingContext(MappingContext);
|
EnhancedInput->RemoveMappingContext(MappingContext);
|
||||||
} else
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
FailedIndices.Add(Index);
|
FailedIndices.Add(Index);
|
||||||
}
|
}
|
||||||
@@ -129,8 +133,11 @@ EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::RemoveInputMappi
|
|||||||
|
|
||||||
if (FailedIndices.Num() > 0)
|
if (FailedIndices.Num() > 0)
|
||||||
{
|
{
|
||||||
FString FailedIndicesStr = FString::JoinBy(FailedIndices, TEXT(", "), [](const int32 Index) { return FString::Printf(TEXT("%d"), Index); });
|
const FString FailedIndicesString = FString::JoinBy(
|
||||||
UE_LOGFMT(LogDirectiveUtil, Warning, "{FailedIndicies} Input Mapping Contexts failed to load and were not removed! The failed indexes are [{FailedIndicieIndexes}]", FailedIndices.Num(), FailedIndicesStr);
|
FailedIndices, TEXT(", "), [](const int32 Index) { return FString::FromInt(Index); });
|
||||||
|
UE_LOGFMT(LogDirectiveUtil, Warning,
|
||||||
|
"{FailedContextCount} input mapping contexts were not loaded and could not be removed. Indexes: [{FailedContextIndexes}]",
|
||||||
|
FailedIndices.Num(), FailedIndicesString);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (FailedIndices.Num() == Contexts.Num())
|
if (FailedIndices.Num() == Contexts.Num())
|
||||||
@@ -143,28 +150,27 @@ EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::RemoveInputMappi
|
|||||||
}
|
}
|
||||||
|
|
||||||
EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::SwapInputMappingContexts(
|
EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::SwapInputMappingContexts(
|
||||||
AController* PlayerController,
|
AController* PlayerController,
|
||||||
const TSoftObjectPtr<UInputMappingContext> PreviousContext,
|
const TSoftObjectPtr<UInputMappingContext> PreviousContext,
|
||||||
const TSoftObjectPtr<UInputMappingContext> NewContext,
|
const TSoftObjectPtr<UInputMappingContext> NewContext,
|
||||||
const int32 Priority,
|
const int32 Priority,
|
||||||
const bool bUsePreviousPriority)
|
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;
|
UEnhancedInputLocalPlayerSubsystem* EnhancedInput;
|
||||||
if (!TryGetEnhancedInputSubsystemFromController(PlayerController, EnhancedInput))
|
if (!TryGetEnhancedInputSubsystemFromController(PlayerController, EnhancedInput))
|
||||||
{
|
{
|
||||||
return EDirectiveUtilSuccessStatus::Failure;
|
return EDirectiveUtilSuccessStatus::Failure;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (int32 PreviousPriority; EnhancedInput->HasMappingContext(LoadedPreviousMappingContext, PreviousPriority))
|
const UInputMappingContext* LoadedNewMappingContext = NewContext.LoadSynchronous();
|
||||||
|
if (!LoadedNewMappingContext)
|
||||||
|
{
|
||||||
|
UE_LOGFMT(LogDirectiveUtil, Warning, "The new input mapping context could not be loaded.");
|
||||||
|
return EDirectiveUtilSuccessStatus::Failure;
|
||||||
|
}
|
||||||
|
const UInputMappingContext* LoadedPreviousMappingContext = PreviousContext.Get();
|
||||||
|
|
||||||
|
if (int32 PreviousPriority; LoadedPreviousMappingContext && EnhancedInput->HasMappingContext(LoadedPreviousMappingContext, PreviousPriority))
|
||||||
{
|
{
|
||||||
const int32 TargetPriority = bUsePreviousPriority ? PreviousPriority : Priority;
|
const int32 TargetPriority = bUsePreviousPriority ? PreviousPriority : Priority;
|
||||||
EnhancedInput->RemoveMappingContext(LoadedPreviousMappingContext);
|
EnhancedInput->RemoveMappingContext(LoadedPreviousMappingContext);
|
||||||
@@ -174,7 +180,7 @@ EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::SwapInputMapping
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
EnhancedInput->AddMappingContext(LoadedNewMappingContext, Priority);
|
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);
|
UE_LOGFMT(LogDirectiveUtil, Verbose, "Previous input mapping context was not active. New context {NewContext} added at priority {BackupPriority}.", LoadedNewMappingContext->GetName(), Priority);
|
||||||
}
|
}
|
||||||
|
|
||||||
return EDirectiveUtilSuccessStatus::Success;
|
return EDirectiveUtilSuccessStatus::Success;
|
||||||
@@ -189,7 +195,7 @@ UEnhancedInputLocalPlayerSubsystem* UDirectiveUtilInputFunctionLibrary::GetEnhan
|
|||||||
|
|
||||||
bool UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(AController* PlayerController, TSoftObjectPtr<UInputMappingContext> Context)
|
bool UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(AController* PlayerController, TSoftObjectPtr<UInputMappingContext> Context)
|
||||||
{
|
{
|
||||||
const UInputMappingContext* MappingContext = Context.LoadSynchronous();
|
const UInputMappingContext* MappingContext = Context.Get();
|
||||||
if (!MappingContext)
|
if (!MappingContext)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -179,7 +179,6 @@ void UDirectiveUtilMapFunctionLibrary::GenericMap_Append(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Appending a map onto itself is a no-op; bail before AddPair can reallocate under the source pointers.
|
|
||||||
if (TargetMap == SourceMap)
|
if (TargetMap == SourceMap)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "Libraries/DirectiveUtilMathFunctionLibrary.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
double EaseBackIn(double t)
|
||||||
|
{
|
||||||
|
if (t <= 0.0) { return 0.0; }
|
||||||
|
if (t >= 1.0) { return 1.0; }
|
||||||
|
const double s = 1.70158;
|
||||||
|
return t * t * ((s + 1.0) * t - s);
|
||||||
|
}
|
||||||
|
|
||||||
|
double EaseBackOut(double t)
|
||||||
|
{
|
||||||
|
if (t <= 0.0) { return 0.0; }
|
||||||
|
if (t >= 1.0) { return 1.0; }
|
||||||
|
const double s = 1.70158;
|
||||||
|
t -= 1.0;
|
||||||
|
return t * t * ((s + 1.0) * t + s) + 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
double EaseBackInOut(double t)
|
||||||
|
{
|
||||||
|
if (t <= 0.0) { return 0.0; }
|
||||||
|
if (t >= 1.0) { return 1.0; }
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
FTransform BlendEasedTransforms(const FTransform& A, const FTransform& B, const double Alpha)
|
||||||
|
{
|
||||||
|
FQuat Rotation = FQuat::Slerp(A.GetRotation(), B.GetRotation(), Alpha);
|
||||||
|
Rotation.Normalize();
|
||||||
|
return FTransform(Rotation,
|
||||||
|
FMath::Lerp(A.GetLocation(), B.GetLocation(), Alpha),
|
||||||
|
FMath::Lerp(A.GetScale3D(), B.GetScale3D(), Alpha));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ValueType, typename BlendType>
|
||||||
|
TArray<ValueType> EaseArrays(const TArray<ValueType>& From, const TArray<ValueType>& To, const float Alpha,
|
||||||
|
const EDirectiveUtilEaseType EaseType, const TArray<float>& PerElementAlphas, BlendType Blend)
|
||||||
|
{
|
||||||
|
const bool bPerElement = !PerElementAlphas.IsEmpty();
|
||||||
|
if (From.Num() != To.Num() || !FMath::IsFinite(Alpha)
|
||||||
|
|| (bPerElement && PerElementAlphas.Num() != From.Num()))
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const float SharedEasedAlpha = UDirectiveUtilMathFunctionLibrary::EaseAlpha(Alpha, EaseType);
|
||||||
|
TArray<ValueType> Result;
|
||||||
|
Result.SetNumUninitialized(From.Num());
|
||||||
|
for (int32 Index = 0; Index < From.Num(); ++Index)
|
||||||
|
{
|
||||||
|
float EasedAlpha = SharedEasedAlpha;
|
||||||
|
if (bPerElement)
|
||||||
|
{
|
||||||
|
if (!FMath::IsFinite(PerElementAlphas[Index]))
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
EasedAlpha = UDirectiveUtilMathFunctionLibrary::EaseAlpha(PerElementAlphas[Index], EaseType);
|
||||||
|
}
|
||||||
|
if (From[Index].ContainsNaN() || To[Index].ContainsNaN())
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
Result[Index] = Blend(From[Index], To[Index], static_cast<double>(EasedAlpha));
|
||||||
|
}
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
float UDirectiveUtilMathFunctionLibrary::EaseAlpha(const float Alpha, const EDirectiveUtilEaseType EaseType)
|
||||||
|
{
|
||||||
|
const double t = static_cast<double>(FMath::Clamp(Alpha, 0.0f, 1.0f));
|
||||||
|
switch (EaseType)
|
||||||
|
{
|
||||||
|
case EDirectiveUtilEaseType::BackIn: return static_cast<float>(EaseBackIn(t));
|
||||||
|
case EDirectiveUtilEaseType::BackOut: return static_cast<float>(EaseBackOut(t));
|
||||||
|
case EDirectiveUtilEaseType::BackInOut: return static_cast<float>(EaseBackInOut(t));
|
||||||
|
case EDirectiveUtilEaseType::ElasticIn: return static_cast<float>(EaseElasticIn(t));
|
||||||
|
case EDirectiveUtilEaseType::ElasticOut: return static_cast<float>(EaseElasticOut(t));
|
||||||
|
case EDirectiveUtilEaseType::ElasticInOut: return static_cast<float>(EaseElasticInOut(t));
|
||||||
|
case EDirectiveUtilEaseType::BounceIn: return static_cast<float>(EaseBounceIn(t));
|
||||||
|
case EDirectiveUtilEaseType::BounceOut: return static_cast<float>(EaseBounceOut(t));
|
||||||
|
case EDirectiveUtilEaseType::BounceInOut: return static_cast<float>(EaseBounceInOut(t));
|
||||||
|
case EDirectiveUtilEaseType::Linear:
|
||||||
|
default: return static_cast<float>(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
FTransform UDirectiveUtilMathFunctionLibrary::EaseTransform(const FTransform& A, const FTransform& B,
|
||||||
|
const float Alpha, const EDirectiveUtilEaseType EaseType)
|
||||||
|
{
|
||||||
|
return BlendEasedTransforms(A, B, static_cast<double>(EaseAlpha(Alpha, EaseType)));
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FVector> UDirectiveUtilMathFunctionLibrary::EaseLocationArrays(const TArray<FVector>& From,
|
||||||
|
const TArray<FVector>& To, const float Alpha, const EDirectiveUtilEaseType EaseType,
|
||||||
|
const TArray<float>& PerElementAlphas)
|
||||||
|
{
|
||||||
|
return EaseArrays(From, To, Alpha, EaseType, PerElementAlphas,
|
||||||
|
[](const FVector& A, const FVector& B, const double EasedAlpha)
|
||||||
|
{
|
||||||
|
return FMath::Lerp(A, B, EasedAlpha);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FTransform> UDirectiveUtilMathFunctionLibrary::EaseTransformArrays(const TArray<FTransform>& From,
|
||||||
|
const TArray<FTransform>& To, const float Alpha, const EDirectiveUtilEaseType EaseType,
|
||||||
|
const TArray<float>& PerElementAlphas)
|
||||||
|
{
|
||||||
|
return EaseArrays(From, To, Alpha, EaseType, PerElementAlphas, &BlendEasedTransforms);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,219 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "Libraries/DirectiveUtilMathFunctionLibrary.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
float GetUsableRandomWeight(const float Weight)
|
||||||
|
{
|
||||||
|
return FMath::IsFinite(Weight) && Weight > 0.0f ? Weight : 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector2D MakeRandomPointInAnnulus(const double InnerRadius, const double OuterRadius,
|
||||||
|
const double AngleSample, const double RadiusSample)
|
||||||
|
{
|
||||||
|
const double Angle = AngleSample * UE_TWO_PI;
|
||||||
|
const double Radius = FMath::Sqrt(FMath::Lerp(
|
||||||
|
InnerRadius * InnerRadius,
|
||||||
|
OuterRadius * OuterRadius,
|
||||||
|
RadiusSample));
|
||||||
|
return FVector2D(FMath::Cos(Angle) * Radius, FMath::Sin(Angle) * Radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename RandomFractionFunction>
|
||||||
|
FVector MakeRandomPointInSphere(const double Radius, RandomFractionFunction&& RandomFraction)
|
||||||
|
{
|
||||||
|
FVector Point;
|
||||||
|
double SizeSquared;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
const double X = static_cast<double>(RandomFraction()) * 2.0 - 1.0;
|
||||||
|
const double Y = static_cast<double>(RandomFraction()) * 2.0 - 1.0;
|
||||||
|
const double Z = static_cast<double>(RandomFraction()) * 2.0 - 1.0;
|
||||||
|
Point = FVector(X, Y, Z);
|
||||||
|
SizeSquared = Point.SizeSquared();
|
||||||
|
}
|
||||||
|
while (SizeSquared > 1.0);
|
||||||
|
|
||||||
|
return Point * Radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights(const TArray<float>& Weights)
|
||||||
|
{
|
||||||
|
double Total = 0.0;
|
||||||
|
for (const float Weight : Weights)
|
||||||
|
{
|
||||||
|
Total += GetUsableRandomWeight(Weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Total <= 0.0)
|
||||||
|
{
|
||||||
|
return INDEX_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double Roll = static_cast<double>(FMath::FRand()) * Total;
|
||||||
|
double Accumulated = 0.0;
|
||||||
|
int32 LastPositiveIndex = INDEX_NONE;
|
||||||
|
for (int32 Index = 0; Index < Weights.Num(); ++Index)
|
||||||
|
{
|
||||||
|
const float Weight = GetUsableRandomWeight(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)
|
||||||
|
{
|
||||||
|
double Total = 0.0;
|
||||||
|
for (const float Weight : Weights)
|
||||||
|
{
|
||||||
|
Total += GetUsableRandomWeight(Weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Total <= 0.0)
|
||||||
|
{
|
||||||
|
return INDEX_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double Roll = static_cast<double>(Stream.FRand()) * Total;
|
||||||
|
double Accumulated = 0.0;
|
||||||
|
int32 LastPositiveIndex = INDEX_NONE;
|
||||||
|
for (int32 Index = 0; Index < Weights.Num(); ++Index)
|
||||||
|
{
|
||||||
|
const float Weight = GetUsableRandomWeight(Weights[Index]);
|
||||||
|
if (Weight <= 0.0f)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
LastPositiveIndex = Index;
|
||||||
|
Accumulated += Weight;
|
||||||
|
if (Roll < Accumulated)
|
||||||
|
{
|
||||||
|
return Index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return LastPositiveIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector2D UDirectiveUtilMathFunctionLibrary::RandomPointInCircle(const float Radius)
|
||||||
|
{
|
||||||
|
if (!FMath::IsFinite(Radius))
|
||||||
|
{
|
||||||
|
return FVector2D::ZeroVector;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double AbsoluteRadius = FMath::Abs(static_cast<double>(Radius));
|
||||||
|
if (AbsoluteRadius == 0.0)
|
||||||
|
{
|
||||||
|
return FVector2D::ZeroVector;
|
||||||
|
}
|
||||||
|
const double AngleSample = FMath::FRand();
|
||||||
|
const double RadiusSample = FMath::FRand();
|
||||||
|
return MakeRandomPointInAnnulus(0.0, AbsoluteRadius, AngleSample, RadiusSample);
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector2D UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream(FRandomStream& Stream, const float Radius)
|
||||||
|
{
|
||||||
|
if (!FMath::IsFinite(Radius))
|
||||||
|
{
|
||||||
|
return FVector2D::ZeroVector;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double AbsoluteRadius = FMath::Abs(static_cast<double>(Radius));
|
||||||
|
if (AbsoluteRadius == 0.0)
|
||||||
|
{
|
||||||
|
return FVector2D::ZeroVector;
|
||||||
|
}
|
||||||
|
const double AngleSample = Stream.FRand();
|
||||||
|
const double RadiusSample = Stream.FRand();
|
||||||
|
return MakeRandomPointInAnnulus(0.0, AbsoluteRadius, AngleSample, RadiusSample);
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector2D UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulus(const float InnerRadius, const float OuterRadius)
|
||||||
|
{
|
||||||
|
if (!FMath::IsFinite(InnerRadius) || !FMath::IsFinite(OuterRadius))
|
||||||
|
{
|
||||||
|
return FVector2D::ZeroVector;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double FirstRadius = FMath::Abs(static_cast<double>(InnerRadius));
|
||||||
|
const double SecondRadius = FMath::Abs(static_cast<double>(OuterRadius));
|
||||||
|
const double Inner = FMath::Min(FirstRadius, SecondRadius);
|
||||||
|
const double Outer = FMath::Max(FirstRadius, SecondRadius);
|
||||||
|
if (Outer == 0.0)
|
||||||
|
{
|
||||||
|
return FVector2D::ZeroVector;
|
||||||
|
}
|
||||||
|
const double AngleSample = FMath::FRand();
|
||||||
|
const double RadiusSample = FMath::FRand();
|
||||||
|
return MakeRandomPointInAnnulus(Inner, Outer, AngleSample, RadiusSample);
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector2D UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream(FRandomStream& Stream,
|
||||||
|
const float InnerRadius, const float OuterRadius)
|
||||||
|
{
|
||||||
|
if (!FMath::IsFinite(InnerRadius) || !FMath::IsFinite(OuterRadius))
|
||||||
|
{
|
||||||
|
return FVector2D::ZeroVector;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double FirstRadius = FMath::Abs(static_cast<double>(InnerRadius));
|
||||||
|
const double SecondRadius = FMath::Abs(static_cast<double>(OuterRadius));
|
||||||
|
const double Inner = FMath::Min(FirstRadius, SecondRadius);
|
||||||
|
const double Outer = FMath::Max(FirstRadius, SecondRadius);
|
||||||
|
if (Outer == 0.0)
|
||||||
|
{
|
||||||
|
return FVector2D::ZeroVector;
|
||||||
|
}
|
||||||
|
const double AngleSample = Stream.FRand();
|
||||||
|
const double RadiusSample = Stream.FRand();
|
||||||
|
return MakeRandomPointInAnnulus(Inner, Outer, AngleSample, RadiusSample);
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector UDirectiveUtilMathFunctionLibrary::RandomPointInSphere(const float Radius)
|
||||||
|
{
|
||||||
|
if (!FMath::IsFinite(Radius))
|
||||||
|
{
|
||||||
|
return FVector::ZeroVector;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double AbsoluteRadius = FMath::Abs(static_cast<double>(Radius));
|
||||||
|
if (AbsoluteRadius == 0.0)
|
||||||
|
{
|
||||||
|
return FVector::ZeroVector;
|
||||||
|
}
|
||||||
|
return MakeRandomPointInSphere(AbsoluteRadius, []
|
||||||
|
{
|
||||||
|
return FMath::FRand();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream(FRandomStream& Stream, const float Radius)
|
||||||
|
{
|
||||||
|
if (!FMath::IsFinite(Radius))
|
||||||
|
{
|
||||||
|
return FVector::ZeroVector;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double AbsoluteRadius = FMath::Abs(static_cast<double>(Radius));
|
||||||
|
if (AbsoluteRadius == 0.0)
|
||||||
|
{
|
||||||
|
return FVector::ZeroVector;
|
||||||
|
}
|
||||||
|
return MakeRandomPointInSphere(AbsoluteRadius, [&Stream]
|
||||||
|
{
|
||||||
|
return Stream.FRand();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,576 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "Libraries/DirectiveUtilMathFunctionLibrary.h"
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
float GetUsableStatisticsWeight(const float Weight)
|
||||||
|
{
|
||||||
|
return FMath::IsFinite(Weight) && Weight > 0.0f ? Weight : 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ValueType>
|
||||||
|
ValueType SelectStatisticsNth(TArray<ValueType>& Values, const int32 NthIndex)
|
||||||
|
{
|
||||||
|
int32 Left = 0;
|
||||||
|
int32 Right = Values.Num() - 1;
|
||||||
|
int32 RemainingDepth = FMath::FloorLog2(static_cast<uint32>(Values.Num())) * 2;
|
||||||
|
while (Left < Right)
|
||||||
|
{
|
||||||
|
if (RemainingDepth-- <= 0)
|
||||||
|
{
|
||||||
|
Values.Sort();
|
||||||
|
return Values[NthIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 Middle = Left + (Right - Left) / 2;
|
||||||
|
if (Values[Middle] < Values[Left])
|
||||||
|
{
|
||||||
|
Values.Swap(Middle, Left);
|
||||||
|
}
|
||||||
|
if (Values[Right] < Values[Left])
|
||||||
|
{
|
||||||
|
Values.Swap(Right, Left);
|
||||||
|
}
|
||||||
|
if (Values[Right] < Values[Middle])
|
||||||
|
{
|
||||||
|
Values.Swap(Right, Middle);
|
||||||
|
}
|
||||||
|
const ValueType Pivot = Values[Middle];
|
||||||
|
|
||||||
|
int32 LessEnd = Left;
|
||||||
|
int32 Current = Left;
|
||||||
|
int32 GreaterStart = Right;
|
||||||
|
while (Current <= GreaterStart)
|
||||||
|
{
|
||||||
|
if (Values[Current] < Pivot)
|
||||||
|
{
|
||||||
|
Values.Swap(LessEnd++, Current++);
|
||||||
|
}
|
||||||
|
else if (Pivot < Values[Current])
|
||||||
|
{
|
||||||
|
Values.Swap(Current, GreaterStart--);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
++Current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NthIndex < LessEnd)
|
||||||
|
{
|
||||||
|
Right = LessEnd - 1;
|
||||||
|
}
|
||||||
|
else if (NthIndex > GreaterStart)
|
||||||
|
{
|
||||||
|
Left = GreaterStart + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Values[NthIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Values[Left];
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ValueType>
|
||||||
|
double CalculateStatisticsMedian(TArray<ValueType>& Values)
|
||||||
|
{
|
||||||
|
const int32 Middle = Values.Num() / 2;
|
||||||
|
const ValueType UpperMiddle = SelectStatisticsNth(Values, Middle);
|
||||||
|
if (Values.Num() % 2 != 0)
|
||||||
|
{
|
||||||
|
return static_cast<double>(UpperMiddle);
|
||||||
|
}
|
||||||
|
|
||||||
|
ValueType LowerMiddle = Values[0];
|
||||||
|
for (int32 Index = 1; Index < Middle; ++Index)
|
||||||
|
{
|
||||||
|
LowerMiddle = FMath::Max(LowerMiddle, Values[Index]);
|
||||||
|
}
|
||||||
|
return (static_cast<double>(LowerMiddle) + static_cast<double>(UpperMiddle)) * 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 double AbsoluteSeconds = FMath::Abs(static_cast<double>(Seconds));
|
||||||
|
const int64 TotalSeconds = AbsoluteSeconds >= static_cast<double>(TNumericLimits<int64>::Max())
|
||||||
|
? TNumericLimits<int64>::Max()
|
||||||
|
: static_cast<int64>(AbsoluteSeconds);
|
||||||
|
const int64 VisibleSeconds = bIncludeSeconds ? TotalSeconds : (TotalSeconds / 60) * 60;
|
||||||
|
const bool bNegative = Seconds < 0.0f && VisibleSeconds > 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 first so timestamps near the current second stay in the expected 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> WorkingValues = Values;
|
||||||
|
return static_cast<float>(CalculateStatisticsMedian(WorkingValues));
|
||||||
|
}
|
||||||
|
|
||||||
|
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> WorkingValues = Values;
|
||||||
|
if (WorkingValues.ContainsByPredicate([](const float Value) { return FMath::IsNaN(Value); }))
|
||||||
|
{
|
||||||
|
return std::numeric_limits<float>::quiet_NaN();
|
||||||
|
}
|
||||||
|
return static_cast<float>(CalculateStatisticsMedian(WorkingValues));
|
||||||
|
}
|
||||||
|
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDirectiveUtilMathFunctionLibrary::GetAngleArrayAverage(const TArray<float>& Angles,
|
||||||
|
float& AverageAngle, float& ResultantStrength)
|
||||||
|
{
|
||||||
|
AverageAngle = 0.0f;
|
||||||
|
ResultantStrength = 0.0f;
|
||||||
|
if (Angles.IsEmpty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
double SineSum = 0.0;
|
||||||
|
double CosineSum = 0.0;
|
||||||
|
for (const float Angle : Angles)
|
||||||
|
{
|
||||||
|
if (!FMath::IsFinite(Angle))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double Radians = FMath::DegreesToRadians(FMath::Fmod(static_cast<double>(Angle), 360.0));
|
||||||
|
SineSum += FMath::Sin(Radians);
|
||||||
|
CosineSum += FMath::Cos(Radians);
|
||||||
|
}
|
||||||
|
|
||||||
|
const double Magnitude = FMath::Sqrt(SineSum * SineSum + CosineSum * CosineSum);
|
||||||
|
ResultantStrength = static_cast<float>(FMath::Clamp(Magnitude / Angles.Num(), 0.0, 1.0));
|
||||||
|
if (ResultantStrength <= UE_DOUBLE_SMALL_NUMBER)
|
||||||
|
{
|
||||||
|
ResultantStrength = 0.0f;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AverageAngle = static_cast<float>(FMath::RadiansToDegrees(FMath::Atan2(SineSum, CosineSum)));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDirectiveUtilMathFunctionLibrary::GetWeightedFloatArrayAverage(const TArray<float>& Values,
|
||||||
|
const TArray<float>& Weights, float& Average)
|
||||||
|
{
|
||||||
|
Average = 0.0f;
|
||||||
|
if (Values.IsEmpty() || Values.Num() != Weights.Num())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
double WeightedSum = 0.0;
|
||||||
|
double WeightSum = 0.0;
|
||||||
|
for (int32 Index = 0; Index < Values.Num(); ++Index)
|
||||||
|
{
|
||||||
|
if (!FMath::IsFinite(Values[Index]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double Weight = GetUsableStatisticsWeight(Weights[Index]);
|
||||||
|
WeightedSum += static_cast<double>(Values[Index]) * Weight;
|
||||||
|
WeightSum += Weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WeightSum <= 0.0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Average = static_cast<float>(WeightedSum / WeightSum);
|
||||||
|
return FMath::IsFinite(Average);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDirectiveUtilMathFunctionLibrary::GetWeightedVectorArrayAverage(const TArray<FVector>& Values,
|
||||||
|
const TArray<float>& Weights, FVector& Average)
|
||||||
|
{
|
||||||
|
Average = FVector::ZeroVector;
|
||||||
|
if (Values.IsEmpty() || Values.Num() != Weights.Num())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector RunningAverage = FVector::ZeroVector;
|
||||||
|
double WeightSum = 0.0;
|
||||||
|
for (int32 Index = 0; Index < Values.Num(); ++Index)
|
||||||
|
{
|
||||||
|
if (Values[Index].ContainsNaN())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double Weight = GetUsableStatisticsWeight(Weights[Index]);
|
||||||
|
if (Weight > 0.0)
|
||||||
|
{
|
||||||
|
const double NewWeightSum = WeightSum + Weight;
|
||||||
|
RunningAverage = FMath::LerpStable(RunningAverage, Values[Index], Weight / NewWeightSum);
|
||||||
|
WeightSum = NewWeightSum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WeightSum <= 0.0 || RunningAverage.ContainsNaN())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Average = RunningAverage;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDirectiveUtilMathFunctionLibrary::NormalizeFloatArrayToRange(const TArray<float>& Values,
|
||||||
|
const float OutputMinimum, const float OutputMaximum, TArray<float>& NormalizedValues)
|
||||||
|
{
|
||||||
|
TArray<float> ValuesCopy;
|
||||||
|
const TArray<float>* SourceValues = &Values;
|
||||||
|
if (&Values == &NormalizedValues)
|
||||||
|
{
|
||||||
|
ValuesCopy = Values;
|
||||||
|
SourceValues = &ValuesCopy;
|
||||||
|
}
|
||||||
|
|
||||||
|
NormalizedValues.Reset();
|
||||||
|
if (SourceValues->IsEmpty() || !FMath::IsFinite(OutputMinimum) || !FMath::IsFinite(OutputMaximum))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
float InputMinimum = (*SourceValues)[0];
|
||||||
|
float InputMaximum = (*SourceValues)[0];
|
||||||
|
for (const float Value : *SourceValues)
|
||||||
|
{
|
||||||
|
if (!FMath::IsFinite(Value))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
InputMinimum = FMath::Min(InputMinimum, Value);
|
||||||
|
InputMaximum = FMath::Max(InputMaximum, Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
NormalizedValues.SetNumUninitialized(SourceValues->Num());
|
||||||
|
if (InputMinimum == InputMaximum)
|
||||||
|
{
|
||||||
|
NormalizedValues.Init(OutputMinimum, SourceValues->Num());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double Scale = (static_cast<double>(OutputMaximum) - OutputMinimum)
|
||||||
|
/ (static_cast<double>(InputMaximum) - InputMinimum);
|
||||||
|
for (int32 Index = 0; Index < SourceValues->Num(); ++Index)
|
||||||
|
{
|
||||||
|
NormalizedValues[Index] = static_cast<float>(OutputMinimum
|
||||||
|
+ (static_cast<double>((*SourceValues)[Index]) - InputMinimum) * Scale);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDirectiveUtilMathFunctionLibrary::NormalizeWeights(const TArray<float>& Weights,
|
||||||
|
TArray<float>& NormalizedWeights)
|
||||||
|
{
|
||||||
|
TArray<float> WeightsCopy;
|
||||||
|
const TArray<float>* SourceWeights = &Weights;
|
||||||
|
if (&Weights == &NormalizedWeights)
|
||||||
|
{
|
||||||
|
WeightsCopy = Weights;
|
||||||
|
SourceWeights = &WeightsCopy;
|
||||||
|
}
|
||||||
|
|
||||||
|
NormalizedWeights.Reset();
|
||||||
|
if (SourceWeights->IsEmpty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
double WeightSum = 0.0;
|
||||||
|
for (const float Weight : *SourceWeights)
|
||||||
|
{
|
||||||
|
WeightSum += GetUsableStatisticsWeight(Weight);
|
||||||
|
}
|
||||||
|
if (WeightSum <= 0.0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
NormalizedWeights.SetNumUninitialized(SourceWeights->Num());
|
||||||
|
for (int32 Index = 0; Index < SourceWeights->Num(); ++Index)
|
||||||
|
{
|
||||||
|
NormalizedWeights[Index] = static_cast<float>(GetUsableStatisticsWeight((*SourceWeights)[Index]) / WeightSum);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile(const TArray<float>& Values,
|
||||||
|
const float Percentile, float& Value)
|
||||||
|
{
|
||||||
|
Value = 0.0f;
|
||||||
|
if (Values.IsEmpty() || !FMath::IsFinite(Percentile))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const float Candidate : Values)
|
||||||
|
{
|
||||||
|
if (!FMath::IsFinite(Candidate))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TArray<float> WorkingValues = Values;
|
||||||
|
|
||||||
|
const double Position = FMath::Clamp(static_cast<double>(Percentile), 0.0, 100.0)
|
||||||
|
* 0.01 * (WorkingValues.Num() - 1);
|
||||||
|
const int32 LowerIndex = FMath::FloorToInt(Position);
|
||||||
|
const int32 UpperIndex = FMath::CeilToInt(Position);
|
||||||
|
const float LowerValue = SelectStatisticsNth(WorkingValues, LowerIndex);
|
||||||
|
if (LowerIndex == UpperIndex)
|
||||||
|
{
|
||||||
|
Value = LowerValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const float UpperValue = SelectStatisticsNth(WorkingValues, UpperIndex);
|
||||||
|
Value = static_cast<float>(FMath::Lerp(
|
||||||
|
static_cast<double>(LowerValue),
|
||||||
|
static_cast<double>(UpperValue),
|
||||||
|
Position - LowerIndex));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDirectiveUtilMathFunctionLibrary::GetFloatArrayRootMeanSquare(const TArray<float>& Values,
|
||||||
|
float& RootMeanSquare)
|
||||||
|
{
|
||||||
|
RootMeanSquare = 0.0f;
|
||||||
|
if (Values.IsEmpty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
double SquaredSum = 0.0;
|
||||||
|
for (const float Value : Values)
|
||||||
|
{
|
||||||
|
if (!FMath::IsFinite(Value))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
SquaredSum += static_cast<double>(Value) * Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
RootMeanSquare = static_cast<float>(FMath::Sqrt(SquaredSum / Values.Num()));
|
||||||
|
return FMath::IsFinite(RootMeanSquare);
|
||||||
|
}
|
||||||
@@ -7,6 +7,10 @@
|
|||||||
#include "GameFramework/SaveGame.h"
|
#include "GameFramework/SaveGame.h"
|
||||||
#include "HAL/FileManager.h"
|
#include "HAL/FileManager.h"
|
||||||
#include "Misc/Paths.h"
|
#include "Misc/Paths.h"
|
||||||
|
#include "PlatformFeatures.h"
|
||||||
|
#include "SaveGameSystem.h"
|
||||||
|
|
||||||
|
#include <ctime>
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -24,11 +28,51 @@ namespace
|
|||||||
{
|
{
|
||||||
return UDirectiveUtilStringFunctionLibrary::IsValidFileName(SlotName);
|
return UDirectiveUtilStringFunctionLibrary::IsValidFileName(SlotName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ISaveGameSystem* GetSaveGameSystem()
|
||||||
|
{
|
||||||
|
return IPlatformFeaturesModule::Get().GetSaveGameSystem();
|
||||||
|
}
|
||||||
|
|
||||||
|
FDateTime ConvertUtcFileTimeToLocal(const FDateTime& UtcTimestamp)
|
||||||
|
{
|
||||||
|
const int64 UnixSeconds = UtcTimestamp.ToUnixTimestamp();
|
||||||
|
const int32 Milliseconds = UtcTimestamp.GetMillisecond();
|
||||||
|
const time_t Time = static_cast<time_t>(UnixSeconds);
|
||||||
|
tm LocalTm;
|
||||||
|
#if PLATFORM_WINDOWS
|
||||||
|
if (localtime_s(&LocalTm, &Time) != 0)
|
||||||
|
{
|
||||||
|
return UtcTimestamp;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if (localtime_r(&Time, &LocalTm) == nullptr)
|
||||||
|
{
|
||||||
|
return UtcTimestamp;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return FDateTime(
|
||||||
|
LocalTm.tm_year + 1900,
|
||||||
|
LocalTm.tm_mon + 1,
|
||||||
|
LocalTm.tm_mday,
|
||||||
|
LocalTm.tm_hour,
|
||||||
|
LocalTm.tm_min,
|
||||||
|
LocalTm.tm_sec,
|
||||||
|
Milliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TArray<FString> UDirectiveUtilSaveGameFunctionLibrary::GetAllSaveSlotNames()
|
TArray<FString> UDirectiveUtilSaveGameFunctionLibrary::GetAllSaveSlotNames()
|
||||||
{
|
{
|
||||||
TArray<FString> SlotNames;
|
TArray<FString> SlotNames;
|
||||||
|
if (ISaveGameSystem* SaveSystem = GetSaveGameSystem())
|
||||||
|
{
|
||||||
|
if (SaveSystem->GetSaveGameNames(SlotNames, 0))
|
||||||
|
{
|
||||||
|
return SlotNames;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TArray<FString> Files;
|
TArray<FString> Files;
|
||||||
IFileManager::Get().FindFiles(Files, *(GetSaveGamesDirectory() / TEXT("*.sav")), true, false);
|
IFileManager::Get().FindFiles(Files, *(GetSaveGamesDirectory() / TEXT("*.sav")), true, false);
|
||||||
@@ -49,13 +93,21 @@ bool UDirectiveUtilSaveGameFunctionLibrary::GetSaveSlotTimestamp(const FString&
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ISaveGameSystem* SaveSystem = GetSaveGameSystem())
|
||||||
|
{
|
||||||
|
if (!SaveSystem->DoesSaveGameExist(*SlotName, 0))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const FDateTime Timestamp = IFileManager::Get().GetTimeStamp(*GetSaveSlotFilePath(SlotName));
|
const FDateTime Timestamp = IFileManager::Get().GetTimeStamp(*GetSaveSlotFilePath(SlotName));
|
||||||
if (Timestamp == FDateTime::MinValue())
|
if (Timestamp == FDateTime::MinValue())
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
OutTimestamp = Timestamp + (FDateTime::Now() - FDateTime::UtcNow());
|
OutTimestamp = ConvertUtcFileTimeToLocal(Timestamp);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,10 +150,30 @@ bool UDirectiveUtilSaveGameFunctionLibrary::DeleteSaveSlot(const FString& SlotNa
|
|||||||
|
|
||||||
bool UDirectiveUtilSaveGameFunctionLibrary::RenameSaveSlot(const FString& OldSlotName, const FString& NewSlotName, const int32 UserIndex)
|
bool UDirectiveUtilSaveGameFunctionLibrary::RenameSaveSlot(const FString& OldSlotName, const FString& NewSlotName, const int32 UserIndex)
|
||||||
{
|
{
|
||||||
if (!IsValidSaveSlotName(OldSlotName) || !IsValidSaveSlotName(NewSlotName) || OldSlotName == NewSlotName)
|
if (!IsValidSaveSlotName(OldSlotName) || !IsValidSaveSlotName(NewSlotName)
|
||||||
|
|| OldSlotName.Equals(NewSlotName, ESearchCase::CaseSensitive))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bool bCaseOnlyRename = OldSlotName.Equals(NewSlotName, ESearchCase::IgnoreCase);
|
||||||
|
if (bCaseOnlyRename)
|
||||||
|
{
|
||||||
|
TArray<uint8> SaveData;
|
||||||
|
if (!UGameplayStatics::LoadDataFromSlot(SaveData, OldSlotName, UserIndex) || SaveData.Num() == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!UGameplayStatics::SaveDataToSlot(SaveData, NewSlotName, UserIndex))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Slot-name case sensitivity belongs to the active platform backend. Deleting
|
||||||
|
// the old spelling here can delete the newly written slot on a case-insensitive
|
||||||
|
// filesystem, so treat both spellings as the same logical slot and rewrite it.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!UGameplayStatics::DoesSaveGameExist(OldSlotName, UserIndex) || UGameplayStatics::DoesSaveGameExist(NewSlotName, UserIndex))
|
if (!UGameplayStatics::DoesSaveGameExist(OldSlotName, UserIndex) || UGameplayStatics::DoesSaveGameExist(NewSlotName, UserIndex))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -14,6 +14,80 @@ namespace
|
|||||||
const FTCHARToUTF8 Converter(*String, String.Len());
|
const FTCHARToUTF8 Converter(*String, String.Len());
|
||||||
return TArray<uint8>(reinterpret_cast<const uint8*>(Converter.Get()), Converter.Length());
|
return TArray<uint8>(reinterpret_cast<const uint8*>(Converter.Get()), Converter.Length());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int32 CalculateLevenshteinDistance(
|
||||||
|
FStringView Left,
|
||||||
|
FStringView Right,
|
||||||
|
TArray<int32>& PreviousRow,
|
||||||
|
TArray<int32>& CurrentRow)
|
||||||
|
{
|
||||||
|
int32 Start = 0;
|
||||||
|
while (Start < Left.Len() && Start < Right.Len() && Left[Start] == Right[Start])
|
||||||
|
{
|
||||||
|
++Start;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 LeftEnd = Left.Len();
|
||||||
|
int32 RightEnd = Right.Len();
|
||||||
|
while (LeftEnd > Start && RightEnd > Start && Left[LeftEnd - 1] == Right[RightEnd - 1])
|
||||||
|
{
|
||||||
|
--LeftEnd;
|
||||||
|
--RightEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 LeftLength = LeftEnd - Start;
|
||||||
|
int32 RightLength = RightEnd - Start;
|
||||||
|
int32 LeftStart = Start;
|
||||||
|
int32 RightStart = Start;
|
||||||
|
if (RightLength > LeftLength)
|
||||||
|
{
|
||||||
|
Swap(Left, Right);
|
||||||
|
Swap(LeftLength, RightLength);
|
||||||
|
Swap(LeftStart, RightStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RightLength == 0)
|
||||||
|
{
|
||||||
|
return LeftLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
PreviousRow.SetNumUninitialized(RightLength + 1);
|
||||||
|
CurrentRow.SetNumUninitialized(RightLength + 1);
|
||||||
|
for (int32 ColumnIndex = 0; ColumnIndex <= RightLength; ++ColumnIndex)
|
||||||
|
{
|
||||||
|
PreviousRow[ColumnIndex] = ColumnIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int32 RowIndex = 1; RowIndex <= LeftLength; ++RowIndex)
|
||||||
|
{
|
||||||
|
CurrentRow[0] = RowIndex;
|
||||||
|
for (int32 ColumnIndex = 1; ColumnIndex <= RightLength; ++ColumnIndex)
|
||||||
|
{
|
||||||
|
const int32 SubstitutionCost = Left[LeftStart + RowIndex - 1] == Right[RightStart + ColumnIndex - 1] ? 0 : 1;
|
||||||
|
CurrentRow[ColumnIndex] = FMath::Min3(
|
||||||
|
PreviousRow[ColumnIndex] + 1,
|
||||||
|
CurrentRow[ColumnIndex - 1] + 1,
|
||||||
|
PreviousRow[ColumnIndex - 1] + SubstitutionCost);
|
||||||
|
}
|
||||||
|
Swap(PreviousRow, CurrentRow);
|
||||||
|
}
|
||||||
|
return PreviousRow[RightLength];
|
||||||
|
}
|
||||||
|
|
||||||
|
float CalculateStringSimilarity(
|
||||||
|
const FStringView Left,
|
||||||
|
const FStringView Right,
|
||||||
|
const int32 MaxLength,
|
||||||
|
TArray<int32>& PreviousRow,
|
||||||
|
TArray<int32>& CurrentRow)
|
||||||
|
{
|
||||||
|
if (MaxLength == 0)
|
||||||
|
{
|
||||||
|
return 1.0f;
|
||||||
|
}
|
||||||
|
const int32 Distance = CalculateLevenshteinDistance(Left, Right, PreviousRow, CurrentRow);
|
||||||
|
return 1.0f - static_cast<float>(Distance) / static_cast<float>(MaxLength);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool UDirectiveUtilStringFunctionLibrary::ContainsLetters(const FString& String)
|
bool UDirectiveUtilStringFunctionLibrary::ContainsLetters(const FString& String)
|
||||||
@@ -219,50 +293,27 @@ TArray<FString> UDirectiveUtilStringFunctionLibrary::GetSortedStringArray(const
|
|||||||
|
|
||||||
int32 UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(const FString& A, const FString& B, const bool bCaseSensitive)
|
int32 UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(const FString& A, const FString& B, const bool bCaseSensitive)
|
||||||
{
|
{
|
||||||
const FString StringA = bCaseSensitive ? A : A.ToLower();
|
const FString NormalizedA = bCaseSensitive ? FString() : A.ToLower();
|
||||||
const FString StringB = bCaseSensitive ? B : B.ToLower();
|
const FString NormalizedB = bCaseSensitive ? FString() : B.ToLower();
|
||||||
const int32 LenA = StringA.Len();
|
const FStringView ViewA = bCaseSensitive ? FStringView(A) : FStringView(NormalizedA);
|
||||||
const int32 LenB = StringB.Len();
|
const FStringView ViewB = bCaseSensitive ? FStringView(B) : FStringView(NormalizedB);
|
||||||
|
|
||||||
if (LenA == 0) { return LenB; }
|
|
||||||
if (LenB == 0) { return LenA; }
|
|
||||||
|
|
||||||
TArray<int32> PreviousRow;
|
TArray<int32> PreviousRow;
|
||||||
TArray<int32> CurrentRow;
|
TArray<int32> CurrentRow;
|
||||||
PreviousRow.SetNumUninitialized(LenB + 1);
|
return CalculateLevenshteinDistance(ViewA, ViewB, PreviousRow, CurrentRow);
|
||||||
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)
|
float UDirectiveUtilStringFunctionLibrary::GetStringSimilarity(const FString& A, const FString& B, const bool bCaseSensitive)
|
||||||
{
|
{
|
||||||
const int32 MaxLength = FMath::Max(A.Len(), B.Len());
|
const FString NormalizedA = bCaseSensitive ? FString() : A.ToLower();
|
||||||
if (MaxLength == 0)
|
const FString NormalizedB = bCaseSensitive ? FString() : B.ToLower();
|
||||||
{
|
TArray<int32> PreviousRow;
|
||||||
return 1.0f;
|
TArray<int32> CurrentRow;
|
||||||
}
|
return CalculateStringSimilarity(
|
||||||
const int32 Distance = GetLevenshteinDistance(A, B, bCaseSensitive);
|
bCaseSensitive ? FStringView(A) : FStringView(NormalizedA),
|
||||||
return 1.0f - (static_cast<float>(Distance) / static_cast<float>(MaxLength));
|
bCaseSensitive ? FStringView(B) : FStringView(NormalizedB),
|
||||||
|
FMath::Max(A.Len(), B.Len()),
|
||||||
|
PreviousRow,
|
||||||
|
CurrentRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool UDirectiveUtilStringFunctionLibrary::ContainsAny(const FString& Source, const TArray<FString>& SearchTerms, const bool bCaseSensitive)
|
bool UDirectiveUtilStringFunctionLibrary::ContainsAny(const FString& Source, const TArray<FString>& SearchTerms, const bool bCaseSensitive)
|
||||||
@@ -394,28 +445,96 @@ int32 UDirectiveUtilStringFunctionLibrary::Crc32Bytes(const TArray<uint8>& Bytes
|
|||||||
return static_cast<int32>(FCrc::MemCrc32(Bytes.GetData(), Bytes.Num()));
|
return static_cast<int32>(FCrc::MemCrc32(Bytes.GetData(), Bytes.Num()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
bool IsReservedDeviceFileName(const FString& FileName)
|
||||||
|
{
|
||||||
|
FString Stem = FileName;
|
||||||
|
int32 DotIndex = INDEX_NONE;
|
||||||
|
if (FileName.FindChar(TEXT('.'), DotIndex))
|
||||||
|
{
|
||||||
|
Stem = FileName.Left(DotIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (Stem.EndsWith(TEXT(".")) || Stem.EndsWith(TEXT(" ")))
|
||||||
|
{
|
||||||
|
Stem.LeftChopInline(1);
|
||||||
|
}
|
||||||
|
if (Stem.IsEmpty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const TCHAR* ReservedNames[] = {
|
||||||
|
TEXT("CON"), TEXT("PRN"), TEXT("AUX"), TEXT("CLOCK$"), TEXT("NUL"),
|
||||||
|
TEXT("COM1"), TEXT("COM2"), TEXT("COM3"), TEXT("COM4"), TEXT("COM5"),
|
||||||
|
TEXT("COM6"), TEXT("COM7"), TEXT("COM8"), TEXT("COM9"),
|
||||||
|
TEXT("LPT1"), TEXT("LPT2"), TEXT("LPT3"), TEXT("LPT4"), TEXT("LPT5"),
|
||||||
|
TEXT("LPT6"), TEXT("LPT7"), TEXT("LPT8"), TEXT("LPT9")
|
||||||
|
};
|
||||||
|
for (const TCHAR* ReservedName : ReservedNames)
|
||||||
|
{
|
||||||
|
if (Stem.Equals(ReservedName, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool UDirectiveUtilStringFunctionLibrary::IsValidFileName(const FString& String)
|
bool UDirectiveUtilStringFunctionLibrary::IsValidFileName(const FString& String)
|
||||||
{
|
{
|
||||||
return !String.IsEmpty() && FPaths::GetCleanFilename(String) == String && SanitizeFileName(String) == String;
|
return !String.IsEmpty()
|
||||||
|
&& String != TEXT(".")
|
||||||
|
&& String != TEXT("..")
|
||||||
|
&& FPaths::GetCleanFilename(String) == String
|
||||||
|
&& SanitizeFileName(String) == String;
|
||||||
}
|
}
|
||||||
|
|
||||||
FString UDirectiveUtilStringFunctionLibrary::SanitizeFileName(const FString& String, const FString& Replacement)
|
FString UDirectiveUtilStringFunctionLibrary::SanitizeFileName(const FString& String, const FString& Replacement)
|
||||||
{
|
{
|
||||||
return FPaths::MakeValidFileName(String, Replacement.IsEmpty() ? TEXT('\0') : Replacement[0]);
|
FString Result = FPaths::MakeValidFileName(String, Replacement.IsEmpty() ? TEXT('\0') : Replacement[0]);
|
||||||
|
while (Result.EndsWith(TEXT(".")) || Result.EndsWith(TEXT(" ")))
|
||||||
|
{
|
||||||
|
Result.LeftChopInline(1);
|
||||||
|
}
|
||||||
|
if (IsReservedDeviceFileName(Result))
|
||||||
|
{
|
||||||
|
Result = FString(TEXT("_")) + Result;
|
||||||
|
}
|
||||||
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
int32 UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(const FString& Input, const TArray<FString>& Candidates, float& OutSimilarity, const bool bCaseSensitive)
|
int32 UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(const FString& Input, const TArray<FString>& Candidates, float& OutSimilarity, const bool bCaseSensitive)
|
||||||
{
|
{
|
||||||
OutSimilarity = 0.0f;
|
OutSimilarity = 0.0f;
|
||||||
int32 BestIndex = INDEX_NONE;
|
int32 BestIndex = INDEX_NONE;
|
||||||
|
const FString NormalizedInput = bCaseSensitive ? FString() : Input.ToLower();
|
||||||
|
const FStringView InputView = bCaseSensitive ? FStringView(Input) : FStringView(NormalizedInput);
|
||||||
|
TArray<int32> PreviousRow;
|
||||||
|
TArray<int32> CurrentRow;
|
||||||
|
|
||||||
for (int32 Index = 0; Index < Candidates.Num(); ++Index)
|
for (int32 Index = 0; Index < Candidates.Num(); ++Index)
|
||||||
{
|
{
|
||||||
const float Similarity = GetStringSimilarity(Input, Candidates[Index], bCaseSensitive);
|
const FString NormalizedCandidate = bCaseSensitive ? FString() : Candidates[Index].ToLower();
|
||||||
|
const FStringView CandidateView = bCaseSensitive
|
||||||
|
? FStringView(Candidates[Index])
|
||||||
|
: FStringView(NormalizedCandidate);
|
||||||
|
const float Similarity = CalculateStringSimilarity(
|
||||||
|
InputView,
|
||||||
|
CandidateView,
|
||||||
|
FMath::Max(Input.Len(), Candidates[Index].Len()),
|
||||||
|
PreviousRow,
|
||||||
|
CurrentRow);
|
||||||
if (BestIndex == INDEX_NONE || Similarity > OutSimilarity)
|
if (BestIndex == INDEX_NONE || Similarity > OutSimilarity)
|
||||||
{
|
{
|
||||||
BestIndex = Index;
|
BestIndex = Index;
|
||||||
OutSimilarity = Similarity;
|
OutSimilarity = Similarity;
|
||||||
|
if (Similarity == 1.0f)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return BestIndex;
|
return BestIndex;
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "Tasks/DirectiveUtilAsyncActionBase.h"
|
||||||
|
|
||||||
|
#include "Engine/Engine.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
|
||||||
|
void UDirectiveUtilAsyncActionBase::RegisterWithGameInstance(const UObject* WorldContextObject)
|
||||||
|
{
|
||||||
|
UnbindWorldCleanup();
|
||||||
|
Super::RegisterWithGameInstance(WorldContextObject);
|
||||||
|
RegisteredWorld = WorldContextObject && GEngine
|
||||||
|
? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull)
|
||||||
|
: nullptr;
|
||||||
|
if (RegisteredWorld.IsValid())
|
||||||
|
{
|
||||||
|
WorldCleanupHandle = FWorldDelegates::OnWorldCleanup.AddUObject(
|
||||||
|
this,
|
||||||
|
&UDirectiveUtilAsyncActionBase::HandleWorldCleanup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilAsyncActionBase::SetReadyToDestroy()
|
||||||
|
{
|
||||||
|
UnbindWorldCleanup();
|
||||||
|
Super::SetReadyToDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilAsyncActionBase::BeginDestroy()
|
||||||
|
{
|
||||||
|
UnbindWorldCleanup();
|
||||||
|
Super::BeginDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilAsyncActionBase::HandleWorldCleanup(
|
||||||
|
UWorld* World,
|
||||||
|
const bool,
|
||||||
|
const bool)
|
||||||
|
{
|
||||||
|
if (World == RegisteredWorld.Get())
|
||||||
|
{
|
||||||
|
SetReadyToDestroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilAsyncActionBase::UnbindWorldCleanup()
|
||||||
|
{
|
||||||
|
if (WorldCleanupHandle.IsValid())
|
||||||
|
{
|
||||||
|
FWorldDelegates::OnWorldCleanup.Remove(WorldCleanupHandle);
|
||||||
|
WorldCleanupHandle.Reset();
|
||||||
|
}
|
||||||
|
RegisteredWorld.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilCancellableAsyncAction::RegisterWithGameInstance(const UObject* WorldContextObject)
|
||||||
|
{
|
||||||
|
UnbindWorldCleanup();
|
||||||
|
Super::RegisterWithGameInstance(WorldContextObject);
|
||||||
|
RegisteredWorld = WorldContextObject && GEngine
|
||||||
|
? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull)
|
||||||
|
: nullptr;
|
||||||
|
if (RegisteredWorld.IsValid())
|
||||||
|
{
|
||||||
|
WorldCleanupHandle = FWorldDelegates::OnWorldCleanup.AddUObject(
|
||||||
|
this,
|
||||||
|
&UDirectiveUtilCancellableAsyncAction::HandleWorldCleanup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilCancellableAsyncAction::SetReadyToDestroy()
|
||||||
|
{
|
||||||
|
UnbindWorldCleanup();
|
||||||
|
Super::SetReadyToDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilCancellableAsyncAction::BeginDestroy()
|
||||||
|
{
|
||||||
|
UnbindWorldCleanup();
|
||||||
|
Super::BeginDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilCancellableAsyncAction::HandleWorldCleanup(
|
||||||
|
UWorld* World,
|
||||||
|
const bool,
|
||||||
|
const bool)
|
||||||
|
{
|
||||||
|
if (World == RegisteredWorld.Get())
|
||||||
|
{
|
||||||
|
Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilCancellableAsyncAction::UnbindWorldCleanup()
|
||||||
|
{
|
||||||
|
if (WorldCleanupHandle.IsValid())
|
||||||
|
{
|
||||||
|
FWorldDelegates::OnWorldCleanup.Remove(WorldCleanupHandle);
|
||||||
|
WorldCleanupHandle.Reset();
|
||||||
|
}
|
||||||
|
RegisteredWorld.Reset();
|
||||||
|
}
|
||||||
@@ -52,7 +52,7 @@ void UDirectiveUtilTask_AsyncLoadAsset::Activate()
|
|||||||
|
|
||||||
void UDirectiveUtilTask_AsyncLoadAsset::OnLoaded()
|
void UDirectiveUtilTask_AsyncLoadAsset::OnLoaded()
|
||||||
{
|
{
|
||||||
UObject* LoadedAsset = StreamableHandle.IsValid() ? StreamableHandle->GetLoadedAsset() : nullptr;
|
UObject* LoadedAsset = SoftAsset.Get();
|
||||||
if (LoadedAsset)
|
if (LoadedAsset)
|
||||||
{
|
{
|
||||||
Completed.Broadcast(LoadedAsset);
|
Completed.Broadcast(LoadedAsset);
|
||||||
@@ -120,7 +120,7 @@ void UDirectiveUtilTask_AsyncLoadClass::Activate()
|
|||||||
|
|
||||||
void UDirectiveUtilTask_AsyncLoadClass::OnLoaded()
|
void UDirectiveUtilTask_AsyncLoadClass::OnLoaded()
|
||||||
{
|
{
|
||||||
UClass* LoadedClass = StreamableHandle.IsValid() ? Cast<UClass>(StreamableHandle->GetLoadedAsset()) : nullptr;
|
UClass* LoadedClass = SoftClass.Get();
|
||||||
if (LoadedClass)
|
if (LoadedClass)
|
||||||
{
|
{
|
||||||
Completed.Broadcast(LoadedClass);
|
Completed.Broadcast(LoadedClass);
|
||||||
@@ -157,8 +157,6 @@ UDirectiveUtilTask_AsyncLoadAssets* UDirectiveUtilTask_AsyncLoadAssets::AsyncLoa
|
|||||||
|
|
||||||
void UDirectiveUtilTask_AsyncLoadAssets::Activate()
|
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;
|
TArray<FSoftObjectPath> PathsToLoad;
|
||||||
for (const TSoftObjectPtr<UObject>& SoftAsset : SoftAssets)
|
for (const TSoftObjectPtr<UObject>& SoftAsset : SoftAssets)
|
||||||
{
|
{
|
||||||
@@ -170,7 +168,6 @@ void UDirectiveUtilTask_AsyncLoadAssets::Activate()
|
|||||||
|
|
||||||
if (PathsToLoad.Num() == 0)
|
if (PathsToLoad.Num() == 0)
|
||||||
{
|
{
|
||||||
// Nothing to request; an empty request list is an error path in the streamable manager.
|
|
||||||
OnLoaded();
|
OnLoaded();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -194,8 +191,6 @@ void UDirectiveUtilTask_AsyncLoadAssets::Activate()
|
|||||||
return;
|
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)))
|
if (!StreamableHandle->BindUpdateDelegate(FStreamableUpdateDelegate::CreateUObject(this, &UDirectiveUtilTask_AsyncLoadAssets::OnUpdate)))
|
||||||
{
|
{
|
||||||
OnLoaded();
|
OnLoaded();
|
||||||
|
|||||||
@@ -25,11 +25,28 @@ UDirectiveUtilTask_Delay* UDirectiveUtilTask_Delay::CancellableDelay(UObject* Wo
|
|||||||
|
|
||||||
void UDirectiveUtilTask_Delay::EndTask()
|
void UDirectiveUtilTask_Delay::EndTask()
|
||||||
{
|
{
|
||||||
if (UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull) : nullptr)
|
Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilTask_Delay::Cancel()
|
||||||
|
{
|
||||||
|
bFinished = true;
|
||||||
|
if (FTimerManager* TimerManager = GetTimerManager())
|
||||||
{
|
{
|
||||||
World->GetTimerManager().ClearTimer(TimerHandle);
|
TimerManager->ClearTimer(TimerHandle);
|
||||||
}
|
}
|
||||||
SetReadyToDestroy();
|
Completed.Clear();
|
||||||
|
Super::Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDirectiveUtilTask_Delay::IsActive() const
|
||||||
|
{
|
||||||
|
return !bFinished && Super::IsActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDirectiveUtilTask_Delay::ShouldBroadcastDelegates() const
|
||||||
|
{
|
||||||
|
return !bFinished && Super::ShouldBroadcastDelegates();
|
||||||
}
|
}
|
||||||
|
|
||||||
void UDirectiveUtilTask_Delay::Activate()
|
void UDirectiveUtilTask_Delay::Activate()
|
||||||
@@ -40,11 +57,14 @@ void UDirectiveUtilTask_Delay::Activate()
|
|||||||
if (!World)
|
if (!World)
|
||||||
{
|
{
|
||||||
UE_LOG(LogDirectiveUtil, Warning, TEXT("Cancellable Delay failed to activate. World is null."));
|
UE_LOG(LogDirectiveUtil, Warning, TEXT("Cancellable Delay failed to activate. World is null."));
|
||||||
|
bFinished = true;
|
||||||
SetReadyToDestroy();
|
SetReadyToDestroy();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const float ClampedDuration = FMath::Max(Duration, KINDA_SMALL_NUMBER);
|
const float ClampedDuration = FMath::IsFinite(Duration)
|
||||||
|
? FMath::Max(Duration, KINDA_SMALL_NUMBER)
|
||||||
|
: KINDA_SMALL_NUMBER;
|
||||||
World->GetTimerManager().SetTimer(TimerHandle, this, &UDirectiveUtilTask_Delay::OnDelayComplete, ClampedDuration, false);
|
World->GetTimerManager().SetTimer(TimerHandle, this, &UDirectiveUtilTask_Delay::OnDelayComplete, ClampedDuration, false);
|
||||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Cancellable Delay started for %f seconds."), ClampedDuration);
|
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Cancellable Delay started for %f seconds."), ClampedDuration);
|
||||||
}
|
}
|
||||||
@@ -52,6 +72,10 @@ void UDirectiveUtilTask_Delay::Activate()
|
|||||||
void UDirectiveUtilTask_Delay::OnDelayComplete()
|
void UDirectiveUtilTask_Delay::OnDelayComplete()
|
||||||
{
|
{
|
||||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Cancellable Delay completed."));
|
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Cancellable Delay completed."));
|
||||||
Completed.Broadcast();
|
if (ShouldBroadcastDelegates())
|
||||||
|
{
|
||||||
|
Completed.Broadcast();
|
||||||
|
}
|
||||||
|
bFinished = true;
|
||||||
SetReadyToDestroy();
|
SetReadyToDestroy();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,286 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "Tasks/DirectiveUtilTask_Flow.h"
|
||||||
|
|
||||||
|
#include "DirectiveUtilLogChannels.h"
|
||||||
|
#include "Engine/Engine.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "TimerManager.h"
|
||||||
|
|
||||||
|
UDirectiveUtilTask_UpdateForDuration* UDirectiveUtilTask_UpdateForDuration::UpdateForDuration(
|
||||||
|
UObject* WorldContextObject,
|
||||||
|
const float Duration,
|
||||||
|
const float UpdateInterval)
|
||||||
|
{
|
||||||
|
UDirectiveUtilTask_UpdateForDuration* Action = NewObject<UDirectiveUtilTask_UpdateForDuration>();
|
||||||
|
Action->WorldContextObject = WorldContextObject;
|
||||||
|
Action->Duration = Duration;
|
||||||
|
Action->UpdateInterval = UpdateInterval;
|
||||||
|
if (WorldContextObject)
|
||||||
|
{
|
||||||
|
Action->RegisterWithGameInstance(WorldContextObject);
|
||||||
|
}
|
||||||
|
return Action;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilTask_UpdateForDuration::Activate()
|
||||||
|
{
|
||||||
|
UWorld* World = WorldContextObject && GEngine
|
||||||
|
? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull)
|
||||||
|
: nullptr;
|
||||||
|
if (!World)
|
||||||
|
{
|
||||||
|
UE_LOG(LogDirectiveUtil, Warning, TEXT("Update for Duration failed to activate. World is null."));
|
||||||
|
bFinished = true;
|
||||||
|
SetReadyToDestroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FTimerManager& TimerManager = World->GetTimerManager();
|
||||||
|
if (!FMath::IsFinite(Duration) || Duration <= 0.0f)
|
||||||
|
{
|
||||||
|
CompletionTimerHandle = TimerManager.SetTimerForNextTick(this, &UDirectiveUtilTask_UpdateForDuration::OnComplete);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TimerManager.SetTimer(
|
||||||
|
CompletionTimerHandle,
|
||||||
|
this,
|
||||||
|
&UDirectiveUtilTask_UpdateForDuration::OnComplete,
|
||||||
|
Duration,
|
||||||
|
false);
|
||||||
|
|
||||||
|
if (!FMath::IsFinite(UpdateInterval) || UpdateInterval <= 0.0f)
|
||||||
|
{
|
||||||
|
UpdateTimerHandle = TimerManager.SetTimerForNextTick(this, &UDirectiveUtilTask_UpdateForDuration::OnUpdate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FTimerManagerTimerParameters UpdateParameters;
|
||||||
|
UpdateParameters.bLoop = true;
|
||||||
|
UpdateParameters.bMaxOncePerFrame = true;
|
||||||
|
UpdateParameters.FirstDelay = UpdateInterval;
|
||||||
|
TimerManager.SetTimer(
|
||||||
|
UpdateTimerHandle,
|
||||||
|
this,
|
||||||
|
&UDirectiveUtilTask_UpdateForDuration::OnUpdate,
|
||||||
|
UpdateInterval,
|
||||||
|
UpdateParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilTask_UpdateForDuration::Cancel()
|
||||||
|
{
|
||||||
|
bFinished = true;
|
||||||
|
ClearTimers();
|
||||||
|
Updated.Clear();
|
||||||
|
Completed.Clear();
|
||||||
|
Super::Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDirectiveUtilTask_UpdateForDuration::IsActive() const
|
||||||
|
{
|
||||||
|
return !bFinished && Super::IsActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDirectiveUtilTask_UpdateForDuration::ShouldBroadcastDelegates() const
|
||||||
|
{
|
||||||
|
return !bFinished && Super::ShouldBroadcastDelegates();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilTask_UpdateForDuration::OnUpdate()
|
||||||
|
{
|
||||||
|
if (!ShouldBroadcastDelegates())
|
||||||
|
{
|
||||||
|
ClearTimers();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FTimerManager* TimerManager = GetTimerManager();
|
||||||
|
if (!TimerManager)
|
||||||
|
{
|
||||||
|
Cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float RemainingTime = TimerManager->GetTimerRemaining(CompletionTimerHandle);
|
||||||
|
const float ElapsedTime = RemainingTime >= 0.0f
|
||||||
|
? FMath::Clamp(Duration - RemainingTime, 0.0f, Duration)
|
||||||
|
: Duration;
|
||||||
|
BroadcastUpdate(ElapsedTime, ElapsedTime / Duration);
|
||||||
|
if (ShouldBroadcastDelegates() && (!FMath::IsFinite(UpdateInterval) || UpdateInterval <= 0.0f))
|
||||||
|
{
|
||||||
|
UpdateTimerHandle = TimerManager->SetTimerForNextTick(this, &UDirectiveUtilTask_UpdateForDuration::OnUpdate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilTask_UpdateForDuration::OnComplete()
|
||||||
|
{
|
||||||
|
if (!ShouldBroadcastDelegates())
|
||||||
|
{
|
||||||
|
ClearTimers();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bHasUpdated || LastElapsedTime < Duration)
|
||||||
|
{
|
||||||
|
const float FinalElapsedTime = FMath::IsFinite(Duration) && Duration > 0.0f ? Duration : 0.0f;
|
||||||
|
BroadcastUpdate(FinalElapsedTime, 1.0f);
|
||||||
|
}
|
||||||
|
if (!ShouldBroadcastDelegates())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClearTimers();
|
||||||
|
Completed.Broadcast();
|
||||||
|
bFinished = true;
|
||||||
|
SetReadyToDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilTask_UpdateForDuration::BroadcastUpdate(const float ElapsedTime, const float Alpha)
|
||||||
|
{
|
||||||
|
const float DeltaTime = bHasUpdated ? FMath::Max(ElapsedTime - LastElapsedTime, 0.0f) : ElapsedTime;
|
||||||
|
LastElapsedTime = ElapsedTime;
|
||||||
|
bHasUpdated = true;
|
||||||
|
Updated.Broadcast(ElapsedTime, DeltaTime, Alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilTask_UpdateForDuration::ClearTimers()
|
||||||
|
{
|
||||||
|
if (FTimerManager* TimerManager = GetTimerManager())
|
||||||
|
{
|
||||||
|
TimerManager->ClearTimer(UpdateTimerHandle);
|
||||||
|
TimerManager->ClearTimer(CompletionTimerHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UDirectiveUtilTask_RepeatWithInterval* UDirectiveUtilTask_RepeatWithInterval::RepeatWithInterval(
|
||||||
|
UObject* WorldContextObject,
|
||||||
|
const int32 Count,
|
||||||
|
const float Interval,
|
||||||
|
const float InitialDelay)
|
||||||
|
{
|
||||||
|
UDirectiveUtilTask_RepeatWithInterval* Action = NewObject<UDirectiveUtilTask_RepeatWithInterval>();
|
||||||
|
Action->WorldContextObject = WorldContextObject;
|
||||||
|
Action->Count = Count;
|
||||||
|
Action->Interval = Interval;
|
||||||
|
Action->InitialDelay = InitialDelay;
|
||||||
|
if (WorldContextObject)
|
||||||
|
{
|
||||||
|
Action->RegisterWithGameInstance(WorldContextObject);
|
||||||
|
}
|
||||||
|
return Action;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilTask_RepeatWithInterval::Activate()
|
||||||
|
{
|
||||||
|
UWorld* World = WorldContextObject && GEngine
|
||||||
|
? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull)
|
||||||
|
: nullptr;
|
||||||
|
if (!World)
|
||||||
|
{
|
||||||
|
UE_LOG(LogDirectiveUtil, Warning, TEXT("Repeat with Interval failed to activate. World is null."));
|
||||||
|
bFinished = true;
|
||||||
|
SetReadyToDestroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Count == 0 || Count < -1)
|
||||||
|
{
|
||||||
|
TimerHandle = World->GetTimerManager().SetTimerForNextTick(this, &UDirectiveUtilTask_RepeatWithInterval::Complete);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float SafeInitialDelay = FMath::IsFinite(InitialDelay) && InitialDelay > 0.0f
|
||||||
|
? InitialDelay
|
||||||
|
: 0.0f;
|
||||||
|
Schedule(SafeInitialDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilTask_RepeatWithInterval::Cancel()
|
||||||
|
{
|
||||||
|
bFinished = true;
|
||||||
|
ClearTimer();
|
||||||
|
Iteration.Clear();
|
||||||
|
Completed.Clear();
|
||||||
|
Super::Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDirectiveUtilTask_RepeatWithInterval::IsActive() const
|
||||||
|
{
|
||||||
|
return !bFinished && Super::IsActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDirectiveUtilTask_RepeatWithInterval::ShouldBroadcastDelegates() const
|
||||||
|
{
|
||||||
|
return !bFinished && Super::ShouldBroadcastDelegates();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilTask_RepeatWithInterval::Schedule(const float Delay)
|
||||||
|
{
|
||||||
|
FTimerManager* TimerManager = GetTimerManager();
|
||||||
|
if (!TimerManager)
|
||||||
|
{
|
||||||
|
Cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Delay > 0.0f)
|
||||||
|
{
|
||||||
|
TimerManager->SetTimer(TimerHandle, this, &UDirectiveUtilTask_RepeatWithInterval::OnIteration, Delay, false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TimerHandle = TimerManager->SetTimerForNextTick(this, &UDirectiveUtilTask_RepeatWithInterval::OnIteration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilTask_RepeatWithInterval::OnIteration()
|
||||||
|
{
|
||||||
|
if (!ShouldBroadcastDelegates())
|
||||||
|
{
|
||||||
|
ClearTimer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 CurrentIndex = NextIndex;
|
||||||
|
NextIndex = NextIndex == MAX_int32 ? 0 : NextIndex + 1;
|
||||||
|
const int32 Remaining = Count == -1 ? -1 : Count - NextIndex;
|
||||||
|
Iteration.Broadcast(CurrentIndex, Remaining);
|
||||||
|
if (!ShouldBroadcastDelegates())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Count != -1 && NextIndex >= Count)
|
||||||
|
{
|
||||||
|
Complete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float SafeInterval = FMath::IsFinite(Interval) && Interval > 0.0f
|
||||||
|
? Interval
|
||||||
|
: 0.0f;
|
||||||
|
Schedule(SafeInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilTask_RepeatWithInterval::Complete()
|
||||||
|
{
|
||||||
|
if (!ShouldBroadcastDelegates())
|
||||||
|
{
|
||||||
|
ClearTimer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClearTimer();
|
||||||
|
Completed.Broadcast();
|
||||||
|
bFinished = true;
|
||||||
|
SetReadyToDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilTask_RepeatWithInterval::ClearTimer()
|
||||||
|
{
|
||||||
|
if (FTimerManager* TimerManager = GetTimerManager())
|
||||||
|
{
|
||||||
|
TimerManager->ClearTimer(TimerHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,22 @@
|
|||||||
#include "Blueprint/AIBlueprintHelperLibrary.h"
|
#include "Blueprint/AIBlueprintHelperLibrary.h"
|
||||||
#include "Engine/World.h"
|
#include "Engine/World.h"
|
||||||
#include "DrawDebugHelpers.h"
|
#include "DrawDebugHelpers.h"
|
||||||
|
#include "NavigationSystem.h"
|
||||||
#include "Navigation/PathFollowingComponent.h"
|
#include "Navigation/PathFollowingComponent.h"
|
||||||
#include "TimerManager.h"
|
#include "TimerManager.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
float GetNonNegativeFiniteValue(const float Value)
|
||||||
|
{
|
||||||
|
return FMath::IsFinite(Value) ? FMath::Max(Value, 0.0f) : 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsWithinAcceptanceRadius(const FVector& CurrentLocation, const FVector& TargetLocation, const float AcceptanceRadius)
|
||||||
|
{
|
||||||
|
return FVector::DistSquared2D(CurrentLocation, TargetLocation) <= FMath::Square(AcceptanceRadius);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
UDirectiveUtilTask_MoveToLocation* UDirectiveUtilTask_MoveToLocation::MoveToLocation(
|
UDirectiveUtilTask_MoveToLocation* UDirectiveUtilTask_MoveToLocation::MoveToLocation(
|
||||||
UObject* WorldContextObject,
|
UObject* WorldContextObject,
|
||||||
@@ -22,9 +35,9 @@ UDirectiveUtilTask_MoveToLocation* UDirectiveUtilTask_MoveToLocation::MoveToLoca
|
|||||||
UDirectiveUtilTask_MoveToLocation* Action = NewObject<UDirectiveUtilTask_MoveToLocation>();
|
UDirectiveUtilTask_MoveToLocation* Action = NewObject<UDirectiveUtilTask_MoveToLocation>();
|
||||||
Action->Controller = Controller;
|
Action->Controller = Controller;
|
||||||
Action->Destination = Destination;
|
Action->Destination = Destination;
|
||||||
Action->AcceptanceRadius = AcceptanceRadius;
|
Action->AcceptanceRadius = GetNonNegativeFiniteValue(AcceptanceRadius);
|
||||||
Action->bDebugLineTrace = bDebugLineTrace;
|
Action->bDebugLineTrace = bDebugLineTrace;
|
||||||
Action->StuckThreshold = StuckThreshold;
|
Action->StuckThreshold = GetNonNegativeFiniteValue(StuckThreshold);
|
||||||
Action->bCheckStuckMovement = bCheckStuckMovement;
|
Action->bCheckStuckMovement = bCheckStuckMovement;
|
||||||
|
|
||||||
if (WorldContextObject)
|
if (WorldContextObject)
|
||||||
@@ -38,27 +51,45 @@ UDirectiveUtilTask_MoveToLocation* UDirectiveUtilTask_MoveToLocation::MoveToLoca
|
|||||||
|
|
||||||
void UDirectiveUtilTask_MoveToLocation::EndTask()
|
void UDirectiveUtilTask_MoveToLocation::EndTask()
|
||||||
{
|
{
|
||||||
|
if (IsValid(Controller))
|
||||||
|
{
|
||||||
|
Controller->StopMovement();
|
||||||
|
}
|
||||||
ExecuteCompleted(false);
|
ExecuteCompleted(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UDirectiveUtilTask_MoveToLocation::Activate()
|
void UDirectiveUtilTask_MoveToLocation::Activate()
|
||||||
{
|
{
|
||||||
if(!Controller || !Controller->GetPawn())
|
if (bHasCompleted)
|
||||||
{
|
{
|
||||||
ExecuteCompleted(false);
|
|
||||||
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller or pawn has been destroyed while moving to location. Aborting."));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
StartLocation = Controller->GetPawn()->GetActorLocation();
|
APawn* Pawn = IsValid(Controller) ? Controller->GetPawn() : nullptr;
|
||||||
|
UWorld* World = IsValid(Pawn) ? Controller->GetWorld() : nullptr;
|
||||||
|
if (!World)
|
||||||
|
{
|
||||||
|
ExecuteCompleted(false);
|
||||||
|
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller, pawn, or world is unavailable while moving to location. Aborting."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!FNavigationSystem::GetCurrent<UNavigationSystemV1>(World))
|
||||||
|
{
|
||||||
|
ExecuteCompleted(false);
|
||||||
|
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Navigation is unavailable while moving to location."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TimerWorld = World;
|
||||||
|
StartLocation = Pawn->GetActorLocation();
|
||||||
LastCheckedLocation = StartLocation;
|
LastCheckedLocation = StartLocation;
|
||||||
CurrentLocation = StartLocation;
|
CurrentLocation = StartLocation;
|
||||||
|
|
||||||
Controller->GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &UDirectiveUtilTask_MoveToLocation::CheckMoveToLocation, 0.1f, true);
|
World->GetTimerManager().SetTimer(TimerHandle, this, &UDirectiveUtilTask_MoveToLocation::CheckMoveToLocation, 0.1f, true);
|
||||||
|
|
||||||
if (bCheckStuckMovement)
|
if (bCheckStuckMovement)
|
||||||
{
|
{
|
||||||
Controller->GetWorld()->GetTimerManager().SetTimer(StuckTimerHandle, this, &UDirectiveUtilTask_MoveToLocation::CheckStuckMovement, 3.f, true);
|
World->GetTimerManager().SetTimer(StuckTimerHandle, this, &UDirectiveUtilTask_MoveToLocation::CheckStuckMovement, 3.f, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
UAIBlueprintHelperLibrary::SimpleMoveToLocation(Controller, Destination);
|
UAIBlueprintHelperLibrary::SimpleMoveToLocation(Controller, Destination);
|
||||||
@@ -67,7 +98,7 @@ void UDirectiveUtilTask_MoveToLocation::Activate()
|
|||||||
if (bDebugLineTrace)
|
if (bDebugLineTrace)
|
||||||
{
|
{
|
||||||
DrawDebugLine(
|
DrawDebugLine(
|
||||||
Controller->GetWorld(),
|
World,
|
||||||
Destination + FVector(0, 0, 100),
|
Destination + FVector(0, 0, 100),
|
||||||
Destination,
|
Destination,
|
||||||
FColor::Green,
|
FColor::Green,
|
||||||
@@ -81,18 +112,18 @@ void UDirectiveUtilTask_MoveToLocation::Activate()
|
|||||||
|
|
||||||
void UDirectiveUtilTask_MoveToLocation::CheckMoveToLocation()
|
void UDirectiveUtilTask_MoveToLocation::CheckMoveToLocation()
|
||||||
{
|
{
|
||||||
|
APawn* Pawn = IsValid(Controller) ? Controller->GetPawn() : nullptr;
|
||||||
if(!Controller || !Controller->GetPawn())
|
if (!IsValid(Pawn) || !TimerWorld.IsValid())
|
||||||
{
|
{
|
||||||
ExecuteCompleted(false);
|
ExecuteCompleted(false);
|
||||||
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller or pawn has been destroyed while moving to location. Aborting."));
|
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller, pawn, or world is unavailable while moving to location. Aborting."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
CurrentLocation = Controller->GetPawn()->GetActorLocation();
|
CurrentLocation = Pawn->GetActorLocation();
|
||||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Controller is moving to location (%s). Current distance: %f."), *Destination.ToString(), FVector::Dist(CurrentLocation, Destination));
|
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)
|
if (IsWithinAcceptanceRadius(CurrentLocation, Destination, AcceptanceRadius))
|
||||||
{
|
{
|
||||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Controller has moved to location."));
|
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Controller has moved to location."));
|
||||||
ExecuteCompleted(true);
|
ExecuteCompleted(true);
|
||||||
@@ -103,16 +134,17 @@ void UDirectiveUtilTask_MoveToLocation::CheckMoveToLocation()
|
|||||||
if (!PathFollowing || PathFollowing->GetStatus() == EPathFollowingStatus::Idle)
|
if (!PathFollowing || PathFollowing->GetStatus() == EPathFollowingStatus::Idle)
|
||||||
{
|
{
|
||||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Path following has stopped. Completing move to location."));
|
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Path following has stopped. Completing move to location."));
|
||||||
ExecuteCompleted(FVector::Dist(CurrentLocation, Destination) < AcceptanceRadius);
|
ExecuteCompleted(IsWithinAcceptanceRadius(CurrentLocation, Destination, AcceptanceRadius));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UDirectiveUtilTask_MoveToLocation::CheckStuckMovement()
|
void UDirectiveUtilTask_MoveToLocation::CheckStuckMovement()
|
||||||
{
|
{
|
||||||
if(!Controller || !Controller->GetPawn())
|
const APawn* Pawn = IsValid(Controller) ? Controller->GetPawn() : nullptr;
|
||||||
|
if (!IsValid(Pawn) || !TimerWorld.IsValid())
|
||||||
{
|
{
|
||||||
ExecuteCompleted(false);
|
ExecuteCompleted(false);
|
||||||
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller or pawn has been destroyed while moving to location. Aborting."));
|
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller, pawn, or world is unavailable while moving to location. Aborting."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,13 +165,14 @@ void UDirectiveUtilTask_MoveToLocation::ExecuteCompleted(const bool bSuccess)
|
|||||||
}
|
}
|
||||||
bHasCompleted = true;
|
bHasCompleted = true;
|
||||||
|
|
||||||
UE_LOG(LogDirectiveUtil, Log, TEXT("Movement to location completed. Success: %s."), bSuccess ? TEXT("true") : TEXT("false"));
|
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Movement to location completed. Success: %s."), bSuccess ? TEXT("true") : TEXT("false"));
|
||||||
|
|
||||||
if (Controller)
|
if (UWorld* World = TimerWorld.Get())
|
||||||
{
|
{
|
||||||
Controller->GetWorld()->GetTimerManager().ClearTimer(TimerHandle);
|
World->GetTimerManager().ClearTimer(TimerHandle);
|
||||||
Controller->GetWorld()->GetTimerManager().ClearTimer(StuckTimerHandle);
|
World->GetTimerManager().ClearTimer(StuckTimerHandle);
|
||||||
}
|
}
|
||||||
|
TimerWorld.Reset();
|
||||||
|
|
||||||
Completed.Broadcast(bSuccess);
|
Completed.Broadcast(bSuccess);
|
||||||
|
|
||||||
@@ -160,8 +193,8 @@ UDirectiveUtilTask_MoveToActor* UDirectiveUtilTask_MoveToActor::MoveToActor(
|
|||||||
UDirectiveUtilTask_MoveToActor* Action = NewObject<UDirectiveUtilTask_MoveToActor>();
|
UDirectiveUtilTask_MoveToActor* Action = NewObject<UDirectiveUtilTask_MoveToActor>();
|
||||||
Action->Controller = Controller;
|
Action->Controller = Controller;
|
||||||
Action->Goal = Goal;
|
Action->Goal = Goal;
|
||||||
Action->AcceptanceRadius = AcceptanceRadius;
|
Action->AcceptanceRadius = GetNonNegativeFiniteValue(AcceptanceRadius);
|
||||||
Action->StuckThreshold = StuckThreshold;
|
Action->StuckThreshold = GetNonNegativeFiniteValue(StuckThreshold);
|
||||||
Action->bCheckStuckMovement = bCheckStuckMovement;
|
Action->bCheckStuckMovement = bCheckStuckMovement;
|
||||||
|
|
||||||
if (WorldContextObject)
|
if (WorldContextObject)
|
||||||
@@ -174,27 +207,45 @@ UDirectiveUtilTask_MoveToActor* UDirectiveUtilTask_MoveToActor::MoveToActor(
|
|||||||
|
|
||||||
void UDirectiveUtilTask_MoveToActor::EndTask()
|
void UDirectiveUtilTask_MoveToActor::EndTask()
|
||||||
{
|
{
|
||||||
|
if (IsValid(Controller))
|
||||||
|
{
|
||||||
|
Controller->StopMovement();
|
||||||
|
}
|
||||||
ExecuteCompleted(false);
|
ExecuteCompleted(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UDirectiveUtilTask_MoveToActor::Activate()
|
void UDirectiveUtilTask_MoveToActor::Activate()
|
||||||
{
|
{
|
||||||
if(!Controller || !Controller->GetPawn() || !IsValid(Goal))
|
if (bHasCompleted)
|
||||||
{
|
{
|
||||||
ExecuteCompleted(false);
|
|
||||||
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller, pawn, or goal has been destroyed while moving to actor. Aborting."));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
StartLocation = Controller->GetPawn()->GetActorLocation();
|
APawn* Pawn = IsValid(Controller) ? Controller->GetPawn() : nullptr;
|
||||||
|
UWorld* World = IsValid(Pawn) ? Controller->GetWorld() : nullptr;
|
||||||
|
if (!World || !IsValid(Goal))
|
||||||
|
{
|
||||||
|
ExecuteCompleted(false);
|
||||||
|
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller, pawn, goal, or world is unavailable while moving to actor. Aborting."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!FNavigationSystem::GetCurrent<UNavigationSystemV1>(World))
|
||||||
|
{
|
||||||
|
ExecuteCompleted(false);
|
||||||
|
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Navigation is unavailable while moving to actor."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TimerWorld = World;
|
||||||
|
StartLocation = Pawn->GetActorLocation();
|
||||||
LastCheckedLocation = StartLocation;
|
LastCheckedLocation = StartLocation;
|
||||||
CurrentLocation = StartLocation;
|
CurrentLocation = StartLocation;
|
||||||
|
|
||||||
Controller->GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &UDirectiveUtilTask_MoveToActor::CheckMoveToActor, 0.1f, true);
|
World->GetTimerManager().SetTimer(TimerHandle, this, &UDirectiveUtilTask_MoveToActor::CheckMoveToActor, 0.1f, true);
|
||||||
|
|
||||||
if (bCheckStuckMovement)
|
if (bCheckStuckMovement)
|
||||||
{
|
{
|
||||||
Controller->GetWorld()->GetTimerManager().SetTimer(StuckTimerHandle, this, &UDirectiveUtilTask_MoveToActor::CheckStuckMovement, 3.f, true);
|
World->GetTimerManager().SetTimer(StuckTimerHandle, this, &UDirectiveUtilTask_MoveToActor::CheckStuckMovement, 3.f, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
UAIBlueprintHelperLibrary::SimpleMoveToActor(Controller, Goal);
|
UAIBlueprintHelperLibrary::SimpleMoveToActor(Controller, Goal);
|
||||||
@@ -203,10 +254,11 @@ void UDirectiveUtilTask_MoveToActor::Activate()
|
|||||||
|
|
||||||
void UDirectiveUtilTask_MoveToActor::CheckMoveToActor()
|
void UDirectiveUtilTask_MoveToActor::CheckMoveToActor()
|
||||||
{
|
{
|
||||||
if(!Controller || !Controller->GetPawn())
|
APawn* Pawn = IsValid(Controller) ? Controller->GetPawn() : nullptr;
|
||||||
|
if (!IsValid(Pawn) || !TimerWorld.IsValid())
|
||||||
{
|
{
|
||||||
ExecuteCompleted(false);
|
ExecuteCompleted(false);
|
||||||
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller or pawn has been destroyed while moving to actor. Aborting."));
|
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller, pawn, or world is unavailable while moving to actor. Aborting."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,10 +271,10 @@ void UDirectiveUtilTask_MoveToActor::CheckMoveToActor()
|
|||||||
|
|
||||||
// The goal can move, so its location is re-read every poll.
|
// The goal can move, so its location is re-read every poll.
|
||||||
const FVector GoalLocation = Goal->GetActorLocation();
|
const FVector GoalLocation = Goal->GetActorLocation();
|
||||||
CurrentLocation = Controller->GetPawn()->GetActorLocation();
|
CurrentLocation = Pawn->GetActorLocation();
|
||||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Controller is moving to actor (%s). Current distance: %f."), *GetNameSafe(Goal), FVector::Dist(CurrentLocation, GoalLocation));
|
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)
|
if (IsWithinAcceptanceRadius(CurrentLocation, GoalLocation, AcceptanceRadius))
|
||||||
{
|
{
|
||||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Controller has moved to actor."));
|
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Controller has moved to actor."));
|
||||||
ExecuteCompleted(true);
|
ExecuteCompleted(true);
|
||||||
@@ -233,16 +285,17 @@ void UDirectiveUtilTask_MoveToActor::CheckMoveToActor()
|
|||||||
if (!PathFollowing || PathFollowing->GetStatus() == EPathFollowingStatus::Idle)
|
if (!PathFollowing || PathFollowing->GetStatus() == EPathFollowingStatus::Idle)
|
||||||
{
|
{
|
||||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Path following has stopped. Completing move to actor."));
|
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Path following has stopped. Completing move to actor."));
|
||||||
ExecuteCompleted(FVector::Dist(CurrentLocation, GoalLocation) < AcceptanceRadius);
|
ExecuteCompleted(IsWithinAcceptanceRadius(CurrentLocation, GoalLocation, AcceptanceRadius));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UDirectiveUtilTask_MoveToActor::CheckStuckMovement()
|
void UDirectiveUtilTask_MoveToActor::CheckStuckMovement()
|
||||||
{
|
{
|
||||||
if(!Controller || !Controller->GetPawn())
|
const APawn* Pawn = IsValid(Controller) ? Controller->GetPawn() : nullptr;
|
||||||
|
if (!IsValid(Pawn) || !TimerWorld.IsValid())
|
||||||
{
|
{
|
||||||
ExecuteCompleted(false);
|
ExecuteCompleted(false);
|
||||||
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller or pawn has been destroyed while moving to actor. Aborting."));
|
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller, pawn, or world is unavailable while moving to actor. Aborting."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,13 +316,14 @@ void UDirectiveUtilTask_MoveToActor::ExecuteCompleted(const bool bSuccess)
|
|||||||
}
|
}
|
||||||
bHasCompleted = true;
|
bHasCompleted = true;
|
||||||
|
|
||||||
UE_LOG(LogDirectiveUtil, Log, TEXT("Movement to actor completed. Success: %s."), bSuccess ? TEXT("true") : TEXT("false"));
|
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Movement to actor completed. Success: %s."), bSuccess ? TEXT("true") : TEXT("false"));
|
||||||
|
|
||||||
if (Controller)
|
if (UWorld* World = TimerWorld.Get())
|
||||||
{
|
{
|
||||||
Controller->GetWorld()->GetTimerManager().ClearTimer(TimerHandle);
|
World->GetTimerManager().ClearTimer(TimerHandle);
|
||||||
Controller->GetWorld()->GetTimerManager().ClearTimer(StuckTimerHandle);
|
World->GetTimerManager().ClearTimer(StuckTimerHandle);
|
||||||
}
|
}
|
||||||
|
TimerWorld.Reset();
|
||||||
|
|
||||||
Completed.Broadcast(bSuccess);
|
Completed.Broadcast(bSuccess);
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,25 @@ public:
|
|||||||
* @param TargetArray - The array to remove duplicates from.
|
* @param TargetArray - The array to remove duplicates from.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Remove Duplicates", CompactNodeTitle = "REMOVE DUPLICATES", ArrayParm = "TargetArray"), Category="Directive Utilities|Array")
|
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Remove Duplicates", CompactNodeTitle = "REMOVE DUPLICATES", ArrayParm = "TargetArray"), Category="Directive Utilities|Array")
|
||||||
static void Array_RemoveDuplicates(const TArray<int32>& TargetArray);
|
static void Array_RemoveDuplicates(UPARAM(ref) TArray<int32>& TargetArray);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends every source element to the target array.
|
||||||
|
* @param TargetArray - The array to append to.
|
||||||
|
* @param SourceArray - The array to append.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Append Array Optimized", CompactNodeTitle = "APPEND", Keywords = "append merge concatenate bulk", ArrayParm = "TargetArray,SourceArray", ArrayTypeDependentParams = "SourceArray"), Category="Directive Utilities|Array")
|
||||||
|
static void Array_AppendOptimized(UPARAM(ref) TArray<int32>& TargetArray, const TArray<int32>& SourceArray);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts every source element into the target array at the given index.
|
||||||
|
* @param TargetArray - The array to insert into.
|
||||||
|
* @param SourceArray - The array to insert.
|
||||||
|
* @param Index - The index at which to insert the source array.
|
||||||
|
* @returns True if one or more elements were inserted.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Insert Array Optimized", CompactNodeTitle = "INSERT ARRAY", Keywords = "insert splice merge bulk", ArrayParm = "TargetArray,SourceArray", ArrayTypeDependentParams = "SourceArray"), Category="Directive Utilities|Array")
|
||||||
|
static bool Array_InsertOptimized(UPARAM(ref) TArray<int32>& TargetArray, const TArray<int32>& SourceArray, const int32 Index);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a copy of the first element of the array.
|
* Returns a copy of the first element of the array.
|
||||||
@@ -106,7 +124,7 @@ public:
|
|||||||
* @returns True if an element was removed.
|
* @returns True if an element was removed.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Pop", ArrayParm = "TargetArray", ArrayTypeDependentParams = "OutItem"), Category="Directive Utilities|Array")
|
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Pop", ArrayParm = "TargetArray", ArrayTypeDependentParams = "OutItem"), Category="Directive Utilities|Array")
|
||||||
static bool Array_Pop(const TArray<int32>& TargetArray, int32& OutItem);
|
static bool Array_Pop(UPARAM(ref) TArray<int32>& TargetArray, int32& OutItem);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the first element of the array and returns a copy of it.
|
* Removes the first element of the array and returns a copy of it.
|
||||||
@@ -115,7 +133,7 @@ public:
|
|||||||
* @returns True if an element was removed.
|
* @returns True if an element was removed.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Pop First", ArrayParm = "TargetArray", ArrayTypeDependentParams = "OutItem"), Category="Directive Utilities|Array")
|
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);
|
static bool Array_PopFirst(UPARAM(ref) TArray<int32>& TargetArray, int32& OutItem);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the element at the given index by swapping it with the last element (does not preserve order).
|
* Removes the element at the given index by swapping it with the last element (does not preserve order).
|
||||||
@@ -125,7 +143,26 @@ public:
|
|||||||
* @returns True if the index was valid and an element was removed.
|
* @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")
|
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);
|
static bool Array_RemoveAtSwap(UPARAM(ref) TArray<int32>& TargetArray, const int32 Index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the elements at the given indices while preserving the order of the remaining elements.
|
||||||
|
* Duplicate and invalid indices are ignored.
|
||||||
|
* @param TargetArray - The array to remove from.
|
||||||
|
* @param Indices - The indices to remove.
|
||||||
|
* @returns The number of elements removed.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Remove At Indices", CompactNodeTitle = "REMOVE INDICES", Keywords = "remove delete batch multiple", ArrayParm = "TargetArray"), Category="Directive Utilities|Array")
|
||||||
|
static int32 Array_RemoveAtIndices(UPARAM(ref) TArray<int32>& TargetArray, const TArray<int32>& Indices);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes every matching item while preserving the order of the remaining elements.
|
||||||
|
* @param TargetArray - The array to remove matching items from.
|
||||||
|
* @param Item - The item to remove.
|
||||||
|
* @returns True if one or more items were removed.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Remove All Occurrences", CompactNodeTitle = "REMOVE ALL", Keywords = "remove item delete matching", ArrayParm = "TargetArray", ArrayTypeDependentParams = "Item", AutoCreateRefTerm = "Item"), Category="Directive Utilities|Array")
|
||||||
|
static bool Array_RemoveAllOccurrences(UPARAM(ref) TArray<int32>& TargetArray, const int32& Item);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a copy of a contiguous range of the array. The range is clamped to the array bounds.
|
* Returns a copy of a contiguous range of the array. The range is clamped to the array bounds.
|
||||||
@@ -143,7 +180,7 @@ public:
|
|||||||
* @param Shift - The number of positions to rotate. Positive rotates toward the end; negative toward the start.
|
* @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")
|
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Rotate", ArrayParm = "TargetArray"), Category="Directive Utilities|Array")
|
||||||
static void Array_Rotate(const TArray<int32>& TargetArray, const int32 Shift);
|
static void Array_Rotate(UPARAM(ref) TArray<int32>& TargetArray, const int32 Shift);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a copy of the array with duplicates removed, keeping the first occurrence and preserving order.
|
* Returns a copy of the array with duplicates removed, keeping the first occurrence and preserving order.
|
||||||
@@ -173,6 +210,72 @@ public:
|
|||||||
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Get Most Common", ArrayParm = "TargetArray", ArrayTypeDependentParams = "OutItem", BlueprintThreadSafe), Category="Directive Utilities|Array")
|
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);
|
static bool Array_GetMostCommon(const TArray<int32>& TargetArray, int32& OutItem, int32& OutCount);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns randomly selected elements from the array.
|
||||||
|
* @param TargetArray The array to sample.
|
||||||
|
* @param Count The requested number of elements, up to 1,000,000.
|
||||||
|
* @param bWithReplacement Whether the same source element can be selected more than once.
|
||||||
|
* @param OutArray The sampled elements.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Sample Array", ArrayParm = "TargetArray,OutArray", ArrayTypeDependentParams = "OutArray"), Category="Directive Utilities|Array")
|
||||||
|
static void Array_Sample(const TArray<int32>& TargetArray, int32 Count, bool bWithReplacement, TArray<int32>& OutArray);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns randomly selected elements using a random stream.
|
||||||
|
* @param TargetArray The array to sample.
|
||||||
|
* @param Count The requested number of elements, up to 1,000,000.
|
||||||
|
* @param bWithReplacement Whether the same source element can be selected more than once.
|
||||||
|
* @param RandomStream The stream used to select elements.
|
||||||
|
* @param OutArray The sampled elements.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Sample Array from Stream", ArrayParm = "TargetArray,OutArray", ArrayTypeDependentParams = "OutArray"), Category="Directive Utilities|Array")
|
||||||
|
static void Array_SampleFromStream(const TArray<int32>& TargetArray, int32 Count, bool bWithReplacement, UPARAM(ref) FRandomStream& RandomStream, TArray<int32>& OutArray);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns randomly selected elements using per-element weights.
|
||||||
|
* @param TargetArray The array to sample.
|
||||||
|
* @param Weights The selection weight for each source element.
|
||||||
|
* @param Count The requested number of elements, up to 1,000,000.
|
||||||
|
* @param bWithReplacement Whether the same source element can be selected more than once.
|
||||||
|
* @param OutArray The sampled elements.
|
||||||
|
* @returns True when the inputs were valid and the sample was produced.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Sample Weighted Array", ArrayParm = "TargetArray,OutArray", ArrayTypeDependentParams = "OutArray"), Category="Directive Utilities|Array")
|
||||||
|
static bool Array_SampleWeighted(const TArray<int32>& TargetArray, const TArray<float>& Weights, int32 Count, bool bWithReplacement, TArray<int32>& OutArray);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns randomly selected elements using per-element weights and a random stream.
|
||||||
|
* @param TargetArray The array to sample.
|
||||||
|
* @param Weights The selection weight for each source element.
|
||||||
|
* @param Count The requested number of elements, up to 1,000,000.
|
||||||
|
* @param bWithReplacement Whether the same source element can be selected more than once.
|
||||||
|
* @param RandomStream The stream used to select elements.
|
||||||
|
* @param OutArray The sampled elements.
|
||||||
|
* @returns True when the inputs were valid and the sample was produced.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Sample Weighted Array from Stream", ArrayParm = "TargetArray,OutArray", ArrayTypeDependentParams = "OutArray"), Category="Directive Utilities|Array")
|
||||||
|
static bool Array_SampleWeightedFromStream(const TArray<int32>& TargetArray, const TArray<float>& Weights, int32 Count, bool bWithReplacement, UPARAM(ref) FRandomStream& RandomStream, TArray<int32>& OutArray);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns one zero-based page from the array.
|
||||||
|
* @param TargetArray The array to read.
|
||||||
|
* @param PageIndex The zero-based page index.
|
||||||
|
* @param PageSize The maximum number of elements per page.
|
||||||
|
* @param OutArray The requested page.
|
||||||
|
* @param OutPageCount The total number of pages.
|
||||||
|
* @returns True when the page index and size are valid.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Get Array Page", ArrayParm = "TargetArray,OutArray", ArrayTypeDependentParams = "OutArray", BlueprintThreadSafe), Category="Directive Utilities|Array")
|
||||||
|
static bool Array_GetPage(const TArray<int32>& TargetArray, int32 PageIndex, int32 PageSize, TArray<int32>& OutArray, int32& OutPageCount);
|
||||||
|
|
||||||
|
/** Sorts strings in natural order so embedded numbers are compared numerically. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category="Directive Utilities|Array")
|
||||||
|
static void NaturalSortStringArray(UPARAM(ref) TArray<FString>& TargetArray, bool bDescending = false);
|
||||||
|
|
||||||
|
/** Sorts names in natural order so embedded numbers are compared numerically. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category="Directive Utilities|Array")
|
||||||
|
static void NaturalSortNameArray(UPARAM(ref) TArray<FName>& TargetArray, bool bDescending = false);
|
||||||
|
|
||||||
|
|
||||||
/*~
|
/*~
|
||||||
* Native functions that will be called by the below custom thunk layers, which read off the property address and call the appropriate native handler.
|
* Native functions that will be called by the below custom thunk layers, which read off the property address and call the appropriate native handler.
|
||||||
@@ -182,6 +285,8 @@ public:
|
|||||||
static int32 GenericArray_NextIndex(const void* TargetArray, const FArrayProperty* ArrayProperty, int32 Index, bool bLoop);
|
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 int32 GenericArray_PreviousIndex(const void* TargetArray, const FArrayProperty* ArrayProperty, int32 Index, bool bLoop);
|
||||||
static void GenericArray_RemoveDuplicates(void* TargetArray, const FArrayProperty* ArrayProperty);
|
static void GenericArray_RemoveDuplicates(void* TargetArray, const FArrayProperty* ArrayProperty);
|
||||||
|
static void GenericArray_AppendOptimized(void* TargetArray, const FArrayProperty* TargetArrayProperty, const void* SourceArray, const FArrayProperty* SourceArrayProperty);
|
||||||
|
static bool GenericArray_InsertOptimized(void* TargetArray, const FArrayProperty* TargetArrayProperty, const void* SourceArray, const FArrayProperty* SourceArrayProperty, int32 Index);
|
||||||
static bool GenericArray_GetItemAtIndex(const void* TargetArray, const FArrayProperty* ArrayProperty, int32 Index, void* OutItemPtr);
|
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_GetFirstItem(const void* TargetArray, const FArrayProperty* ArrayProperty, void* OutItemPtr);
|
||||||
static bool GenericArray_GetLastItem(const void* TargetArray, const FArrayProperty* ArrayProperty, void* OutItemPtr);
|
static bool GenericArray_GetLastItem(const void* TargetArray, const FArrayProperty* ArrayProperty, void* OutItemPtr);
|
||||||
@@ -189,11 +294,16 @@ public:
|
|||||||
static bool GenericArray_Pop(void* TargetArray, const FArrayProperty* ArrayProperty, void* OutItemPtr);
|
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_PopFirst(void* TargetArray, const FArrayProperty* ArrayProperty, void* OutItemPtr);
|
||||||
static bool GenericArray_RemoveAtSwap(void* TargetArray, const FArrayProperty* ArrayProperty, int32 Index);
|
static bool GenericArray_RemoveAtSwap(void* TargetArray, const FArrayProperty* ArrayProperty, int32 Index);
|
||||||
|
static int32 GenericArray_RemoveAtIndices(void* TargetArray, const FArrayProperty* ArrayProperty, const TArray<int32>& Indices);
|
||||||
|
static bool GenericArray_RemoveAllOccurrences(void* TargetArray, const FArrayProperty* ArrayProperty, const void* Item);
|
||||||
static void GenericArray_Slice(const void* TargetArray, const FArrayProperty* TargetArrayProperty, int32 StartIndex, int32 Count, void* OutArray, const FArrayProperty* OutArrayProperty);
|
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_Rotate(void* TargetArray, const FArrayProperty* ArrayProperty, int32 Shift);
|
||||||
static void GenericArray_GetDistinct(const void* TargetArray, const FArrayProperty* TargetArrayProperty, void* OutArray, const FArrayProperty* OutArrayProperty);
|
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 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);
|
static bool GenericArray_GetMostCommon(const void* TargetArray, const FArrayProperty* ArrayProperty, void* OutItemPtr, int32* OutCount);
|
||||||
|
static void GenericArray_Sample(const void* TargetArray, const FArrayProperty* TargetArrayProperty, int32 Count, bool bWithReplacement, FRandomStream* RandomStream, void* OutArray, const FArrayProperty* OutArrayProperty);
|
||||||
|
static bool GenericArray_SampleWeighted(const void* TargetArray, const FArrayProperty* TargetArrayProperty, const TArray<float>& Weights, int32 Count, bool bWithReplacement, FRandomStream* RandomStream, void* OutArray, const FArrayProperty* OutArrayProperty);
|
||||||
|
static bool GenericArray_GetPage(const void* TargetArray, const FArrayProperty* TargetArrayProperty, int32 PageIndex, int32 PageSize, void* OutArray, const FArrayProperty* OutArrayProperty, int32* OutPageCount);
|
||||||
|
|
||||||
/*~
|
/*~
|
||||||
* Custom thunk layers that read off the property address and call the appropriate native handler.
|
* Custom thunk layers that read off the property address and call the appropriate native handler.
|
||||||
@@ -258,6 +368,78 @@ public:
|
|||||||
P_NATIVE_END;
|
P_NATIVE_END;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DECLARE_FUNCTION(execArray_AppendOptimized)
|
||||||
|
{
|
||||||
|
Stack.MostRecentProperty = nullptr;
|
||||||
|
Stack.StepCompiledIn<FArrayProperty>(nullptr);
|
||||||
|
void* TargetArrayAddr = Stack.MostRecentPropertyAddress;
|
||||||
|
FArrayProperty* TargetArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
|
||||||
|
if (!TargetArrayProperty)
|
||||||
|
{
|
||||||
|
Stack.bArrayContextFailed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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_FINISH;
|
||||||
|
P_NATIVE_BEGIN;
|
||||||
|
MARK_PROPERTY_DIRTY(Stack.Object, TargetArrayProperty);
|
||||||
|
GenericArray_AppendOptimized(
|
||||||
|
TargetArrayAddr,
|
||||||
|
TargetArrayProperty,
|
||||||
|
SourceArrayAddr,
|
||||||
|
SourceArrayProperty);
|
||||||
|
P_NATIVE_END;
|
||||||
|
}
|
||||||
|
|
||||||
|
DECLARE_FUNCTION(execArray_InsertOptimized)
|
||||||
|
{
|
||||||
|
Stack.MostRecentProperty = nullptr;
|
||||||
|
Stack.StepCompiledIn<FArrayProperty>(nullptr);
|
||||||
|
void* TargetArrayAddr = Stack.MostRecentPropertyAddress;
|
||||||
|
FArrayProperty* TargetArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
|
||||||
|
if (!TargetArrayProperty)
|
||||||
|
{
|
||||||
|
Stack.bArrayContextFailed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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, Index);
|
||||||
|
P_FINISH;
|
||||||
|
P_NATIVE_BEGIN;
|
||||||
|
const bool bInserted = GenericArray_InsertOptimized(
|
||||||
|
TargetArrayAddr,
|
||||||
|
TargetArrayProperty,
|
||||||
|
SourceArrayAddr,
|
||||||
|
SourceArrayProperty,
|
||||||
|
Index);
|
||||||
|
if (bInserted)
|
||||||
|
{
|
||||||
|
MARK_PROPERTY_DIRTY(Stack.Object, TargetArrayProperty);
|
||||||
|
}
|
||||||
|
*static_cast<bool*>(RESULT_PARAM) = bInserted;
|
||||||
|
P_NATIVE_END;
|
||||||
|
}
|
||||||
|
|
||||||
DECLARE_FUNCTION(execArray_GetValidFirstItemCopy)
|
DECLARE_FUNCTION(execArray_GetValidFirstItemCopy)
|
||||||
{
|
{
|
||||||
Stack.MostRecentProperty = nullptr;
|
Stack.MostRecentProperty = nullptr;
|
||||||
@@ -565,6 +747,70 @@ public:
|
|||||||
P_NATIVE_END;
|
P_NATIVE_END;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DECLARE_FUNCTION(execArray_RemoveAtIndices)
|
||||||
|
{
|
||||||
|
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_TARRAY_REF(int32, Indices);
|
||||||
|
P_FINISH;
|
||||||
|
P_NATIVE_BEGIN;
|
||||||
|
const int32 RemovedCount = GenericArray_RemoveAtIndices(ArrayAddr, ArrayProperty, Indices);
|
||||||
|
if (RemovedCount > 0)
|
||||||
|
{
|
||||||
|
MARK_PROPERTY_DIRTY(Stack.Object, ArrayProperty);
|
||||||
|
}
|
||||||
|
*static_cast<int32*>(RESULT_PARAM) = RemovedCount;
|
||||||
|
P_NATIVE_END;
|
||||||
|
}
|
||||||
|
|
||||||
|
DECLARE_FUNCTION(execArray_RemoveAllOccurrences)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
P_FINISH;
|
||||||
|
|
||||||
|
if (const FBoolProperty* BoolProperty = CastField<const FBoolProperty>(InnerProp))
|
||||||
|
{
|
||||||
|
ensure(PropertySize == sizeof(uint8));
|
||||||
|
BoolProperty->SetPropertyValue(StorageSpace, *static_cast<uint8*>(StorageSpace) != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
P_NATIVE_BEGIN;
|
||||||
|
const bool bRemoved = GenericArray_RemoveAllOccurrences(ArrayAddr, ArrayProperty, StorageSpace);
|
||||||
|
if (bRemoved)
|
||||||
|
{
|
||||||
|
MARK_PROPERTY_DIRTY(Stack.Object, ArrayProperty);
|
||||||
|
}
|
||||||
|
*static_cast<bool*>(RESULT_PARAM) = bRemoved;
|
||||||
|
P_NATIVE_END;
|
||||||
|
|
||||||
|
InnerProp->DestroyValue(StorageSpace);
|
||||||
|
}
|
||||||
|
|
||||||
DECLARE_FUNCTION(execArray_Slice)
|
DECLARE_FUNCTION(execArray_Slice)
|
||||||
{
|
{
|
||||||
Stack.MostRecentProperty = nullptr;
|
Stack.MostRecentProperty = nullptr;
|
||||||
@@ -713,4 +959,179 @@ public:
|
|||||||
P_NATIVE_END;
|
P_NATIVE_END;
|
||||||
InnerProp->DestroyValue(StorageSpace);
|
InnerProp->DestroyValue(StorageSpace);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DECLARE_FUNCTION(execArray_Sample)
|
||||||
|
{
|
||||||
|
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, Count);
|
||||||
|
P_GET_UBOOL(bWithReplacement);
|
||||||
|
|
||||||
|
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_Sample(SourceArrayAddr, SourceArrayProperty, Count, bWithReplacement, nullptr, OutArrayAddr, OutArrayProperty);
|
||||||
|
P_NATIVE_END;
|
||||||
|
}
|
||||||
|
|
||||||
|
DECLARE_FUNCTION(execArray_SampleFromStream)
|
||||||
|
{
|
||||||
|
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, Count);
|
||||||
|
P_GET_UBOOL(bWithReplacement);
|
||||||
|
P_GET_STRUCT_REF(FRandomStream, RandomStream);
|
||||||
|
|
||||||
|
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_Sample(SourceArrayAddr, SourceArrayProperty, Count, bWithReplacement, &RandomStream, OutArrayAddr, OutArrayProperty);
|
||||||
|
P_NATIVE_END;
|
||||||
|
}
|
||||||
|
|
||||||
|
DECLARE_FUNCTION(execArray_SampleWeighted)
|
||||||
|
{
|
||||||
|
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_TARRAY_REF(float, Weights);
|
||||||
|
P_GET_PROPERTY(FIntProperty, Count);
|
||||||
|
P_GET_UBOOL(bWithReplacement);
|
||||||
|
|
||||||
|
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;
|
||||||
|
*static_cast<bool*>(RESULT_PARAM) = GenericArray_SampleWeighted(
|
||||||
|
SourceArrayAddr,
|
||||||
|
SourceArrayProperty,
|
||||||
|
Weights,
|
||||||
|
Count,
|
||||||
|
bWithReplacement,
|
||||||
|
nullptr,
|
||||||
|
OutArrayAddr,
|
||||||
|
OutArrayProperty);
|
||||||
|
P_NATIVE_END;
|
||||||
|
}
|
||||||
|
|
||||||
|
DECLARE_FUNCTION(execArray_SampleWeightedFromStream)
|
||||||
|
{
|
||||||
|
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_TARRAY_REF(float, Weights);
|
||||||
|
P_GET_PROPERTY(FIntProperty, Count);
|
||||||
|
P_GET_UBOOL(bWithReplacement);
|
||||||
|
P_GET_STRUCT_REF(FRandomStream, RandomStream);
|
||||||
|
|
||||||
|
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;
|
||||||
|
*static_cast<bool*>(RESULT_PARAM) = GenericArray_SampleWeighted(
|
||||||
|
SourceArrayAddr,
|
||||||
|
SourceArrayProperty,
|
||||||
|
Weights,
|
||||||
|
Count,
|
||||||
|
bWithReplacement,
|
||||||
|
&RandomStream,
|
||||||
|
OutArrayAddr,
|
||||||
|
OutArrayProperty);
|
||||||
|
P_NATIVE_END;
|
||||||
|
}
|
||||||
|
|
||||||
|
DECLARE_FUNCTION(execArray_GetPage)
|
||||||
|
{
|
||||||
|
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, PageIndex);
|
||||||
|
P_GET_PROPERTY(FIntProperty, PageSize);
|
||||||
|
|
||||||
|
Stack.MostRecentProperty = nullptr;
|
||||||
|
Stack.StepCompiledIn<FArrayProperty>(nullptr);
|
||||||
|
void* OutArrayAddr = Stack.MostRecentPropertyAddress;
|
||||||
|
const FArrayProperty* OutArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
|
||||||
|
if (!OutArrayProperty)
|
||||||
|
{
|
||||||
|
Stack.bArrayContextFailed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stack.MostRecentProperty = nullptr;
|
||||||
|
Stack.MostRecentPropertyAddress = nullptr;
|
||||||
|
Stack.StepCompiledIn<FProperty>(nullptr);
|
||||||
|
int32* OutPageCount = reinterpret_cast<int32*>(Stack.MostRecentPropertyAddress);
|
||||||
|
|
||||||
|
P_FINISH;
|
||||||
|
P_NATIVE_BEGIN;
|
||||||
|
*static_cast<bool*>(RESULT_PARAM) = GenericArray_GetPage(SourceArrayAddr, SourceArrayProperty, PageIndex, PageSize, OutArrayAddr, OutArrayProperty, OutPageCount);
|
||||||
|
P_NATIVE_END;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||||
|
#include "Types/DirectiveUtilTypes.h"
|
||||||
#include "DirectiveUtilFunctionLibrary.generated.h"
|
#include "DirectiveUtilFunctionLibrary.generated.h"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,14 +47,14 @@ public:
|
|||||||
* Get the content from the clipboard as FText.
|
* Get the content from the clipboard as FText.
|
||||||
* @returns The text from the clipboard.
|
* @returns The text from the clipboard.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Clipboard" )
|
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Clipboard" )
|
||||||
static FText GetTextFromClipboard();
|
static FText GetTextFromClipboard();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the content from the clipboard as an FString.
|
* Get the content from the clipboard as an FString.
|
||||||
* @returns The content from the clipboard as a string.
|
* @returns The content from the clipboard as a string.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Clipboard" )
|
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Clipboard" )
|
||||||
static FString GetStringFromClipboard();
|
static FString GetStringFromClipboard();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,6 +78,28 @@ public:
|
|||||||
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Utility")
|
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Utility")
|
||||||
static bool IsRunningInEditor();
|
static bool IsRunningInEditor();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the type of world associated with the supplied context.
|
||||||
|
* @param WorldContextObject Object used to resolve the current world.
|
||||||
|
* @returns The resolved world type, or Unknown when the context has no world.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Utility", meta = (WorldContext = "WorldContextObject"))
|
||||||
|
static EDirectiveUtilWorldType GetWorldType(const UObject* WorldContextObject);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the build configuration of the running application.
|
||||||
|
* @returns The active build configuration.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Utility", meta = (BlueprintThreadSafe))
|
||||||
|
static EDirectiveUtilBuildConfiguration GetBuildConfigurationType();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the build target type of the running application.
|
||||||
|
* @returns The active build target type.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Utility", meta = (BlueprintThreadSafe))
|
||||||
|
static EDirectiveUtilBuildTargetType GetBuildTargetType();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether a switch (e.g. "MySwitch" matching "-MySwitch") was passed on the
|
* Checks whether a switch (e.g. "MySwitch" matching "-MySwitch") was passed on the
|
||||||
* process command line. Matching is case-insensitive.
|
* process command line. Matching is case-insensitive.
|
||||||
@@ -96,6 +119,24 @@ public:
|
|||||||
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Utility")
|
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Utility")
|
||||||
static bool GetCommandLineOption(const FString& Key, FString& OutValue);
|
static bool GetCommandLineOption(const FString& Key, FString& OutValue);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a keyed stopwatch using monotonic real time.
|
||||||
|
* @param Key The name used to stop this stopwatch.
|
||||||
|
* @param bRestartIfRunning Whether to replace an active stopwatch with the same key.
|
||||||
|
* @returns True when the stopwatch was started.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Utility|Profiling", meta = (DisplayName = "Start Stopwatch", Keywords = "timer profiling benchmark elapsed milliseconds"))
|
||||||
|
static bool StartStopwatch(FName Key, bool bRestartIfRunning = false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops a keyed stopwatch and returns its elapsed real time.
|
||||||
|
* @param Key The name passed to Start Stopwatch.
|
||||||
|
* @param ElapsedMilliseconds The elapsed time in milliseconds, or zero when the key is not active.
|
||||||
|
* @returns True when an active stopwatch was found.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Utility|Profiling", meta = (DisplayName = "Stop Stopwatch", Keywords = "timer profiling benchmark elapsed milliseconds"))
|
||||||
|
static bool StopStopwatch(FName Key, double& ElapsedMilliseconds);
|
||||||
|
|
||||||
/** Core of Has Command Line Switch that checks an explicit command line. */
|
/** Core of Has Command Line Switch that checks an explicit command line. */
|
||||||
static bool HasCommandLineSwitch(const TCHAR* CommandLine, const FString& Switch);
|
static bool HasCommandLineSwitch(const TCHAR* CommandLine, const FString& Switch);
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ public:
|
|||||||
* @param Tag - The tag to read.
|
* @param Tag - The tag to read.
|
||||||
* @returns A container of the tag's ancestors.
|
* @returns A container of the tag's ancestors.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags", meta = (BlueprintThreadSafe))
|
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags")
|
||||||
static FGameplayTagContainer GetTagParents(const FGameplayTag& Tag);
|
static FGameplayTagContainer GetTagParents(const FGameplayTag& Tag);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,7 +65,7 @@ public:
|
|||||||
* @param Tag - The tag to read.
|
* @param Tag - The tag to read.
|
||||||
* @returns A container of the tag's descendants, or an empty container for an invalid tag.
|
* @returns A container of the tag's descendants, or an empty container for an invalid tag.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags", meta = (BlueprintThreadSafe))
|
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags")
|
||||||
static FGameplayTagContainer GetTagChildren(const FGameplayTag& Tag);
|
static FGameplayTagContainer GetTagChildren(const FGameplayTag& Tag);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,7 +73,7 @@ public:
|
|||||||
* @param Tag - The tag to read.
|
* @param Tag - The tag to read.
|
||||||
* @returns A container of the tag's direct children, or an empty container for an invalid tag.
|
* @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))
|
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags")
|
||||||
static FGameplayTagContainer GetTagDirectChildren(const FGameplayTag& Tag);
|
static FGameplayTagContainer GetTagDirectChildren(const FGameplayTag& Tag);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -110,7 +110,7 @@ public:
|
|||||||
* @param Tag - The tag to test.
|
* @param Tag - The tag to test.
|
||||||
* @returns True if the tag is valid and has no registered descendants; false for an invalid tag.
|
* @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))
|
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags")
|
||||||
static bool IsLeafTag(const FGameplayTag& Tag);
|
static bool IsLeafTag(const FGameplayTag& Tag);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public:
|
|||||||
/**
|
/**
|
||||||
* Remove multiple Input Mapping Contexts.
|
* 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 PlayerController The player controller to remove the contexts from. Will attempt to get the LocalPlayer from the controller.
|
||||||
* @param Contexts The contexts to remove.
|
* @param Contexts Loaded contexts to remove. This function does not load missing assets.
|
||||||
* @returns Returns Success if the contexts were successfully removed.
|
* @returns Returns Success if the contexts were successfully removed.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Input", meta=(ExpandEnumAsExecs="ReturnValue", DefaultToSelf="PlayerController"))
|
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Input", meta=(ExpandEnumAsExecs="ReturnValue", DefaultToSelf="PlayerController"))
|
||||||
@@ -45,17 +45,17 @@ public:
|
|||||||
const TArray<TSoftObjectPtr<UInputMappingContext>>& Contexts);
|
const TArray<TSoftObjectPtr<UInputMappingContext>>& Contexts);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Swap a designated Input Mapping Context with a new one.
|
* 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 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.
|
* 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 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 PreviousContext The context to swap out.
|
||||||
* @param NewContext The context to swap in.
|
* @param NewContext The context to swap in. This asset is loaded synchronously when needed.
|
||||||
* @param Priority The priority to set the new context to.
|
* @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.
|
* @param bUsePreviousPriority Whether to use the previous context's priority when adding the new context.
|
||||||
* @returns Returns Success if the contexts were successfully swapped.
|
* @returns Returns Success if the contexts were successfully swapped.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Input", meta=(ExpandEnumAsExecs="ReturnValue", DefaultToSelf="PlayerController"))
|
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Input", meta=(ExpandEnumAsExecs="ReturnValue", DefaultToSelf="PlayerController"))
|
||||||
static EDirectiveUtilSuccessStatus SwapInputMappingContexts(
|
static EDirectiveUtilSuccessStatus SwapInputMappingContexts(
|
||||||
AController* PlayerController,
|
AController* PlayerController,
|
||||||
TSoftObjectPtr<UInputMappingContext> PreviousContext,
|
TSoftObjectPtr<UInputMappingContext> PreviousContext,
|
||||||
@@ -74,7 +74,7 @@ public:
|
|||||||
/**
|
/**
|
||||||
* Returns whether the given input mapping context is currently active on the controller.
|
* Returns whether the given input mapping context is currently active on the controller.
|
||||||
* @param PlayerController - The player controller to query.
|
* @param PlayerController - The player controller to query.
|
||||||
* @param Context - The input mapping context to check.
|
* @param Context - The loaded input mapping context to check. This function does not load missing assets.
|
||||||
* @returns True if the context is currently applied.
|
* @returns True if the context is currently applied.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Input", meta=(DefaultToSelf="PlayerController"))
|
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Input", meta=(DefaultToSelf="PlayerController"))
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ public:
|
|||||||
* @param SourceMap - The map to copy from.
|
* @param SourceMap - The map to copy from.
|
||||||
* @param bOverwriteExisting - If true, keys already present in TargetMap are overwritten with SourceMap's values.
|
* @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")
|
UFUNCTION(BlueprintCallable, CustomThunk, meta=(BlueprintInternalUseOnly = "true", 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);
|
static void Map_Append(const TMap<int32, int32>& TargetMap, const TMap<int32, int32>& SourceMap, bool bOverwriteExisting = true);
|
||||||
|
|
||||||
/*~
|
/*~
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
|
#include "Components/SplineComponent.h"
|
||||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||||
#include "Types/DirectiveUtilMathTypes.h"
|
#include "Types/DirectiveUtilMathTypes.h"
|
||||||
#include "DirectiveUtilMathFunctionLibrary.generated.h"
|
#include "DirectiveUtilMathFunctionLibrary.generated.h"
|
||||||
@@ -18,6 +19,7 @@ class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilMathFunctionLibrary : public U
|
|||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
static constexpr int32 MaximumGeneratedElementCount = 1000000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a perlin noise value between -1 and 1 at the given position.
|
* Returns a perlin noise value between -1 and 1 at the given position.
|
||||||
@@ -41,18 +43,507 @@ public:
|
|||||||
* Returns the angle in degrees between two vectors.
|
* Returns the angle in degrees between two vectors.
|
||||||
* @param A - The first vector.
|
* @param A - The first vector.
|
||||||
* @param B - The second vector.
|
* @param B - The second vector.
|
||||||
* @returns The angle between the two vectors in degrees.
|
* @returns The angle between the two vectors in degrees, or 0 if either
|
||||||
|
* vector is zero or non-finite.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Vector")
|
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Vector")
|
||||||
static float AngleBetweenVectors(const FVector& A, const FVector& B);
|
static float AngleBetweenVectors(const FVector& A, const FVector& B);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the signed angle in degrees from one vector to another around an axis.
|
||||||
|
* The vectors are projected onto the plane perpendicular to the axis before measuring.
|
||||||
|
* @param From - The starting direction.
|
||||||
|
* @param To - The target direction.
|
||||||
|
* @param Axis - The axis that defines the rotation plane and positive direction.
|
||||||
|
* @returns The signed angle in the [-180, 180] range, or 0 if an input cannot define a direction.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Signed Angle Between Vectors", BlueprintThreadSafe), Category = "Directive Utilities|Math|Vector")
|
||||||
|
static float SignedAngleBetweenVectors(const FVector& From, const FVector& To, const FVector& Axis);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the shortest signed difference in degrees from one angle to another.
|
||||||
|
* Exactly opposite angles always return +180, regardless of how the inputs are spelled.
|
||||||
|
* @param From - The starting angle in degrees.
|
||||||
|
* @param To - The target angle in degrees.
|
||||||
|
* @returns The signed difference in the (-180, 180] range, or 0 for non-finite input.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Delta Angle (Degrees)", BlueprintThreadSafe), Category = "Directive Utilities|Math|Float")
|
||||||
|
static float DeltaAngle(float From, float To);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interpolates between two angles along the shortest path.
|
||||||
|
* Alpha 0 returns A. Values outside [0, 1] extrapolate along that same
|
||||||
|
* shortest-path direction without wrapping, so a timeline past the end
|
||||||
|
* does not jump the seam.
|
||||||
|
* @param A - The starting angle in degrees.
|
||||||
|
* @param B - The target angle in degrees.
|
||||||
|
* @param Alpha - The interpolation alpha. Values outside [0, 1] extrapolate.
|
||||||
|
* @returns A plus the shortest signed delta to B, scaled by Alpha, or 0 for non-finite input.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Lerp Angle (Degrees)", BlueprintThreadSafe), Category = "Directive Utilities|Math|Float")
|
||||||
|
static float LerpAngle(float A, float B, float Alpha);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repeats a value between two bounds, reversing direction at each bound.
|
||||||
|
* @param Value - The value to repeat.
|
||||||
|
* @param Minimum - One range bound.
|
||||||
|
* @param Maximum - The other range bound.
|
||||||
|
* @returns The ping-ponged value, the shared bound for a zero-sized range, or 0 for non-finite input.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Ping Pong (Float)", BlueprintThreadSafe), Category = "Directive Utilities|Math|Float")
|
||||||
|
static float PingPong(float Value, float Minimum = 0.0f, float Maximum = 1.0f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies cubic smoothing to a value between two bounds.
|
||||||
|
* @returns A value in the [0, 1] range, or 0 for non-finite input.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Smooth Step", BlueprintThreadSafe), Category = "Directive Utilities|Math|Float")
|
||||||
|
static float SmoothStep(float Value, float Minimum = 0.0f, float Maximum = 1.0f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies quintic smoothing to a value between two bounds.
|
||||||
|
* @returns A value in the [0, 1] range, or 0 for non-finite input.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Smoother Step", BlueprintThreadSafe), Category = "Directive Utilities|Math|Float")
|
||||||
|
static float SmootherStep(float Value, float Minimum = 0.0f, float Maximum = 1.0f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a normalized falloff between an inner and outer radius.
|
||||||
|
* @returns 1 at or inside the inner radius, 0 beyond the outer radius, or 0 for non-finite input.
|
||||||
|
* Equal radii are a step: 1 at or inside the shared radius, 0 outside.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Range Falloff", BlueprintThreadSafe), Category = "Directive Utilities|Math|Float")
|
||||||
|
static float RangeFalloff(float Distance, float InnerRadius, float OuterRadius, float FalloffExponent = 1.0f);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests whether a direction lies within a cone centered on another direction.
|
||||||
|
* @param Direction - The direction to test.
|
||||||
|
* @param ConeDirection - The center direction of the cone.
|
||||||
|
* @param ConeHalfAngleDegrees - The angle from the cone center to its edge. Clamped to [0, 180].
|
||||||
|
* @returns True when the direction lies inside or on the cone, or false for an invalid direction or angle.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Is Direction Within Cone", BlueprintThreadSafe), Category = "Directive Utilities|Math|Vector")
|
||||||
|
static bool IsDirectionWithinCone(const FVector& Direction, const FVector& ConeDirection, float ConeHalfAngleDegrees);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the normalized direction and distance from one point to another.
|
||||||
|
* @returns False when the points are equal or an input is non-finite.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Direction And Distance", BlueprintThreadSafe), Category = "Directive Utilities|Math|Vector")
|
||||||
|
static bool GetDirectionAndDistance(const FVector& From, const FVector& To, FVector& Direction, double& Distance);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotates a 2D point around a pivot in degrees.
|
||||||
|
* @returns The rotated point, or zero for non-finite input.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Rotate Point Around Pivot 2D", BlueprintThreadSafe), Category = "Directive Utilities|Math|Vector")
|
||||||
|
static FVector2D RotatePointAroundPivot2D(const FVector2D& Point, const FVector2D& Pivot, float AngleDegrees);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the signed distance from a point to a plane.
|
||||||
|
* @returns The signed distance, or 0 when the plane normal is zero or an input is non-finite.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Signed Distance To Plane", BlueprintThreadSafe), Category = "Directive Utilities|Math|Vector")
|
||||||
|
static double SignedDistanceToPlane(const FVector& Point, const FVector& PlanePoint, const FVector& PlaneNormal);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests whether a point lies within a cone and optional maximum distance.
|
||||||
|
* @returns True when the point lies inside or on the cone and within the distance limit.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Is Point Within Cone", BlueprintThreadSafe), Category = "Directive Utilities|Math|Vector")
|
||||||
|
static bool IsPointWithinCone(const FVector& Point, const FVector& ConeOrigin, const FVector& ConeDirection,
|
||||||
|
float ConeHalfAngleDegrees, double MaximumDistance = 0.0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Samples a location along the polyline through an array, with Alpha 0 at the first point and 1 at the last.
|
||||||
|
* Progress is distance-weighted, so equal alpha steps cover equal distance.
|
||||||
|
* A closed loop adds the segment from the last point back to the first and wraps Alpha instead of clamping it.
|
||||||
|
* @returns The sampled location, or the zero vector for an empty array or non-finite input.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Sample Location Array", BlueprintThreadSafe), Category = "Directive Utilities|Math|Vector")
|
||||||
|
static FVector SampleLocationArray(const TArray<FVector>& Locations, float Alpha, bool bClosedLoop = false);
|
||||||
|
|
||||||
|
/** Creates one transform per location using a shared rotation and scale. */
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Locations To Transforms", BlueprintThreadSafe), Category = "Directive Utilities|Math|Transform")
|
||||||
|
static TArray<FTransform> LocationsToTransforms(const TArray<FVector>& Locations,
|
||||||
|
FRotator Rotation = FRotator(0.0, 0.0, 0.0), FVector Scale = FVector(1.0, 1.0, 1.0));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates one transform per location with its local X axis facing toward or away from a target.
|
||||||
|
* A location equal to Target uses Rotation Offset without a facing rotation.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Locations To Facing Transforms", BlueprintThreadSafe, AdvancedDisplay = "UpDirection,RotationOffset,Scale,bFaceAway"), Category = "Directive Utilities|Math|Transform")
|
||||||
|
static TArray<FTransform> LocationsToFacingTransforms(const TArray<FVector>& Locations,
|
||||||
|
FVector Target, FVector UpDirection = FVector(0.0, 0.0, 1.0),
|
||||||
|
FRotator RotationOffset = FRotator(0.0, 0.0, 0.0), FVector Scale = FVector(1.0, 1.0, 1.0),
|
||||||
|
bool bFaceAway = false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates transforms from location, rotation, and scale arrays.
|
||||||
|
* Rotation and scale arrays may be empty, contain one value to broadcast, or match the location count.
|
||||||
|
* @returns True when the attribute-array lengths and values are valid.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Make Transforms From Arrays", AutoCreateRefTerm = "Rotations,Scales", BlueprintThreadSafe), Category = "Directive Utilities|Math|Transform")
|
||||||
|
static bool MakeTransformsFromArrays(const TArray<FVector>& Locations, const TArray<FRotator>& Rotations,
|
||||||
|
const TArray<FVector>& Scales, TArray<FTransform>& Transforms);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Samples a transform along the path through an array, with Alpha 0 at the first transform and 1 at the last.
|
||||||
|
* Progress is distance-weighted by location. Rotation takes the shortest path and scale interpolates linearly.
|
||||||
|
* A closed loop adds the segment from the last transform back to the first and wraps Alpha instead of clamping it.
|
||||||
|
* @returns The sampled transform, or the identity for an empty array or non-finite input.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Sample Transform Array", BlueprintThreadSafe), Category = "Directive Utilities|Math|Transform")
|
||||||
|
static FTransform SampleTransformArray(const TArray<FTransform>& Transforms, float Alpha, bool bClosedLoop = false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a rectangular grid on the local XY plane.
|
||||||
|
* @param Origin - The first point, or the grid center when Centered is true.
|
||||||
|
* @param Rotation - The grid plane rotation.
|
||||||
|
* @param Dimensions - The number of points along the local X and Y axes.
|
||||||
|
* @param Spacing - The signed center-to-center spacing along the local X and Y axes.
|
||||||
|
* @param bCentered - Whether to center the grid on Origin.
|
||||||
|
* @returns Points ordered by X, then Y, or an empty array for invalid input or an unsupported point count.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Generate Grid Points 2D"), Category = "Directive Utilities|Math|Point Generation")
|
||||||
|
static TArray<FVector> GenerateGridPoints2D(const FVector& Origin, const FRotator& Rotation,
|
||||||
|
FIntPoint Dimensions, const FVector2D& Spacing, bool bCentered = true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a rectangular 3D grid.
|
||||||
|
* @param Origin - The first point, or the grid center when Centered is true.
|
||||||
|
* @param Rotation - The grid rotation.
|
||||||
|
* @param Dimensions - The number of points along the local X, Y, and Z axes.
|
||||||
|
* @param Spacing - The signed center-to-center spacing along the local X, Y, and Z axes.
|
||||||
|
* @param bCentered - Whether to center the grid on Origin.
|
||||||
|
* @returns Points ordered by X, then Y, then Z, or an empty array for invalid input or an unsupported point count.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Generate Grid Points 3D"), Category = "Directive Utilities|Math|Point Generation")
|
||||||
|
static TArray<FVector> GenerateGridPoints3D(const FVector& Origin, const FRotator& Rotation,
|
||||||
|
FIntVector Dimensions, const FVector& Spacing, bool bCentered = true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates transforms on a rectangular grid on the local XY plane.
|
||||||
|
* @param Origin - The first location, or the grid center when Centered is true.
|
||||||
|
* @param Rotation - The grid plane rotation.
|
||||||
|
* @param Dimensions - The number of points along the local X and Y axes.
|
||||||
|
* @param Spacing - The signed center-to-center spacing along the local X and Y axes.
|
||||||
|
* @param bCentered - Whether to center the grid on Origin.
|
||||||
|
* @param InstanceRotation - Shared rotation applied to every transform.
|
||||||
|
* @param Scale - Shared scale applied to every transform.
|
||||||
|
* @returns Transforms ordered by X, then Y, or an empty array for invalid input or an unsupported count.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Generate Grid Transforms 2D", AdvancedDisplay = "InstanceRotation,Scale"), Category = "Directive Utilities|Math|Point Generation")
|
||||||
|
static TArray<FTransform> GenerateGridTransforms2D(const FVector& Origin, const FRotator& Rotation,
|
||||||
|
FIntPoint Dimensions, const FVector2D& Spacing, bool bCentered = true,
|
||||||
|
FRotator InstanceRotation = FRotator(0.0, 0.0, 0.0), FVector Scale = FVector(1.0, 1.0, 1.0));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates transforms on a rectangular 3D grid.
|
||||||
|
* @param Origin - The first location, or the grid center when Centered is true.
|
||||||
|
* @param Rotation - The grid rotation.
|
||||||
|
* @param Dimensions - The number of points along the local X, Y, and Z axes.
|
||||||
|
* @param Spacing - The signed center-to-center spacing along the local X, Y, and Z axes.
|
||||||
|
* @param bCentered - Whether to center the grid on Origin.
|
||||||
|
* @param InstanceRotation - Shared rotation applied to every transform.
|
||||||
|
* @param Scale - Shared scale applied to every transform.
|
||||||
|
* @returns Transforms ordered by X, then Y, then Z, or an empty array for invalid input or an unsupported count.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Generate Grid Transforms 3D", AdvancedDisplay = "InstanceRotation,Scale"), Category = "Directive Utilities|Math|Point Generation")
|
||||||
|
static TArray<FTransform> GenerateGridTransforms3D(const FVector& Origin, const FRotator& Rotation,
|
||||||
|
FIntVector Dimensions, const FVector& Spacing, bool bCentered = true,
|
||||||
|
FRotator InstanceRotation = FRotator(0.0, 0.0, 0.0), FVector Scale = FVector(1.0, 1.0, 1.0));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a rectangular hex grid on the rotated local XY plane.
|
||||||
|
* @param Origin - The first cell center, or the grid bounds center when Centered is true.
|
||||||
|
* @param Rotation - The grid plane rotation.
|
||||||
|
* @param Dimensions - The number of columns and rows.
|
||||||
|
* @param CellRadius - The distance from a cell center to a corner. Must be positive.
|
||||||
|
* @param Orientation - Whether the hex cells have pointy or flat tops.
|
||||||
|
* @param Gap - The signed edge-to-edge gap between adjacent cells. Negative values overlap cells.
|
||||||
|
* @param bCentered - Whether to center the grid bounds on Origin.
|
||||||
|
* @returns Points ordered by row, then column, or an empty array for invalid input or an unsupported point count.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Generate Rectangular Hex Grid"), Category = "Directive Utilities|Math|Hex Grid")
|
||||||
|
static TArray<FVector> GenerateRectangularHexGrid(const FVector& Origin, const FRotator& Rotation,
|
||||||
|
FIntPoint Dimensions, double CellRadius,
|
||||||
|
EDirectiveUtilHexOrientation Orientation = EDirectiveUtilHexOrientation::PointyTop,
|
||||||
|
double Gap = 0.0, bool bCentered = true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates transforms for a rectangular hex grid with a shared instance rotation and scale.
|
||||||
|
* Cell order matches Generate Rectangular Hex Grid.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Generate Rectangular Hex Grid Transforms", AdvancedDisplay = "InstanceRotation,Scale"), Category = "Directive Utilities|Math|Hex Grid")
|
||||||
|
static TArray<FTransform> GenerateRectangularHexGridTransforms(const FVector& Origin, const FRotator& Rotation,
|
||||||
|
FIntPoint Dimensions, double CellRadius,
|
||||||
|
EDirectiveUtilHexOrientation Orientation = EDirectiveUtilHexOrientation::PointyTop,
|
||||||
|
double Gap = 0.0, bool bCentered = true,
|
||||||
|
FRotator InstanceRotation = FRotator(0.0, 0.0, 0.0), FVector Scale = FVector(1.0, 1.0, 1.0));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the axial coordinate of every cell of a rectangular hex grid, in the same cell order as
|
||||||
|
* Generate Rectangular Hex Grid.
|
||||||
|
* @returns The coordinates, or an empty array for invalid input or an unsupported count.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Get Rectangular Hex Grid Coordinates"), Category = "Directive Utilities|Math|Hex Grid")
|
||||||
|
static TArray<FIntPoint> GetRectangularHexGridCoordinates(FIntPoint Dimensions,
|
||||||
|
EDirectiveUtilHexOrientation Orientation = EDirectiveUtilHexOrientation::PointyTop);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a hexagon-shaped grid on the rotated local XY plane.
|
||||||
|
* @param Origin - The center cell location.
|
||||||
|
* @param Rotation - The grid plane rotation.
|
||||||
|
* @param GridRadius - The number of cell rings around the center cell.
|
||||||
|
* @param CellRadius - The distance from a cell center to a corner. Must be positive.
|
||||||
|
* @param Orientation - Whether the hex cells have pointy or flat tops.
|
||||||
|
* @param Gap - The signed edge-to-edge gap between adjacent cells. Negative values overlap cells.
|
||||||
|
* @returns Points ordered by axial R, then Q, or an empty array for invalid input or an unsupported point count.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Generate Hexagonal Hex Grid"), Category = "Directive Utilities|Math|Hex Grid")
|
||||||
|
static TArray<FVector> GenerateHexagonalHexGrid(const FVector& Origin, const FRotator& Rotation,
|
||||||
|
int32 GridRadius, double CellRadius,
|
||||||
|
EDirectiveUtilHexOrientation Orientation = EDirectiveUtilHexOrientation::PointyTop,
|
||||||
|
double Gap = 0.0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates transforms for a hexagon-shaped grid with a shared instance rotation and scale.
|
||||||
|
* Cell order matches Generate Hexagonal Hex Grid.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Generate Hexagonal Hex Grid Transforms", AdvancedDisplay = "InstanceRotation,Scale"), Category = "Directive Utilities|Math|Hex Grid")
|
||||||
|
static TArray<FTransform> GenerateHexagonalHexGridTransforms(const FVector& Origin, const FRotator& Rotation,
|
||||||
|
int32 GridRadius, double CellRadius,
|
||||||
|
EDirectiveUtilHexOrientation Orientation = EDirectiveUtilHexOrientation::PointyTop,
|
||||||
|
double Gap = 0.0,
|
||||||
|
FRotator InstanceRotation = FRotator(0.0, 0.0, 0.0), FVector Scale = FVector(1.0, 1.0, 1.0));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an axial hex coordinate to a location on the rotated local XY plane.
|
||||||
|
* @returns The cell center, or the zero vector for invalid layout input or coordinate overflow.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Hex Coordinate To Location", BlueprintThreadSafe), Category = "Directive Utilities|Math|Hex Grid")
|
||||||
|
static FVector HexCoordinateToLocation(FIntPoint Coordinate, const FVector& Origin, const FRotator& Rotation,
|
||||||
|
double CellRadius, EDirectiveUtilHexOrientation Orientation = EDirectiveUtilHexOrientation::PointyTop,
|
||||||
|
double Gap = 0.0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the axial coordinate of the nearest hex after projecting a location onto the rotated local XY plane.
|
||||||
|
* @returns The nearest axial coordinate, or (0, 0) for invalid layout input or an unrepresentable coordinate.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Location To Hex Coordinate", BlueprintThreadSafe), Category = "Directive Utilities|Math|Hex Grid")
|
||||||
|
static FIntPoint LocationToHexCoordinate(const FVector& Location, const FVector& Origin,
|
||||||
|
const FRotator& Rotation, double CellRadius,
|
||||||
|
EDirectiveUtilHexOrientation Orientation = EDirectiveUtilHexOrientation::PointyTop,
|
||||||
|
double Gap = 0.0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the six adjacent axial coordinates in a stable direction order.
|
||||||
|
* @returns Six neighbors, or an empty array when a neighbor would exceed the FIntPoint range.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Hex Neighbors", BlueprintThreadSafe), Category = "Directive Utilities|Math|Hex Grid")
|
||||||
|
static TArray<FIntPoint> GetHexNeighbors(FIntPoint Coordinate);
|
||||||
|
|
||||||
|
/** Returns the number of hex-grid steps between two axial coordinates. */
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Hex Distance", BlueprintThreadSafe), Category = "Directive Utilities|Math|Hex Grid")
|
||||||
|
static int64 GetHexDistance(FIntPoint A, FIntPoint B);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns every axial coordinate within a number of steps of a center cell, ordered by axial R, then Q.
|
||||||
|
* With a zero center the order matches the cells of Generate Hexagonal Hex Grid.
|
||||||
|
* @returns The coordinates, or an empty array for a negative range, coordinate overflow, or an unsupported count.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Get Hexes In Range"), Category = "Directive Utilities|Math|Hex Grid")
|
||||||
|
static TArray<FIntPoint> GetHexesInRange(FIntPoint Center, int32 Range);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the axial coordinates exactly Radius steps from a center cell.
|
||||||
|
* Consecutive entries are adjacent and trace the ring once. A radius of zero returns the center.
|
||||||
|
* @returns The ring coordinates, or an empty array for a negative radius or coordinate overflow.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Get Hex Ring"), Category = "Directive Utilities|Math|Hex Grid")
|
||||||
|
static TArray<FIntPoint> GetHexRing(FIntPoint Center, int32 Radius);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the axial coordinates along the straight line between two cells, including both endpoints.
|
||||||
|
* @returns The line coordinates, or an empty array for an unsupported length.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Get Hex Line"), Category = "Directive Utilities|Math|Hex Grid")
|
||||||
|
static TArray<FIntPoint> GetHexLine(FIntPoint Start, FIntPoint End);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the six corner locations of a hex cell on the rotated local XY plane, ordered counter-clockwise.
|
||||||
|
* Corners lie at Cell Radius from the cell center; Gap only moves the center.
|
||||||
|
* @returns The corner locations, or an empty array for invalid layout input or coordinate overflow.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Hex Cell Corners", BlueprintThreadSafe), Category = "Directive Utilities|Math|Hex Grid")
|
||||||
|
static TArray<FVector> GetHexCellCorners(FIntPoint Coordinate, const FVector& Origin, const FRotator& Rotation,
|
||||||
|
double CellRadius, EDirectiveUtilHexOrientation Orientation = EDirectiveUtilHexOrientation::PointyTop,
|
||||||
|
double Gap = 0.0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates points at a fixed spacing along a direction.
|
||||||
|
* @param Origin - The first point, or the formation center when Centered is true.
|
||||||
|
* @param Direction - The direction of travel. Its magnitude is ignored.
|
||||||
|
* @param Count - The number of points to generate.
|
||||||
|
* @param Spacing - The signed center-to-center distance between points.
|
||||||
|
* @param bCentered - Whether to center the formation on Origin.
|
||||||
|
* @returns The generated points, or an empty array for invalid input.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Generate Points Along Direction"), Category = "Directive Utilities|Math|Point Generation")
|
||||||
|
static TArray<FVector> GeneratePointsAlongDirection(const FVector& Origin, const FVector& Direction,
|
||||||
|
int32 Count, double Spacing, bool bCentered = false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates evenly spaced points between two locations.
|
||||||
|
* @param Start - The start of the segment.
|
||||||
|
* @param End - The end of the segment.
|
||||||
|
* @param Count - The number of points to generate.
|
||||||
|
* @param bIncludeEndpoints - Whether the generated points include Start and End.
|
||||||
|
* @returns The generated points, or an empty array for invalid input.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Generate Points Between Locations"), Category = "Directive Utilities|Math|Point Generation")
|
||||||
|
static TArray<FVector> GeneratePointsBetweenLocations(const FVector& Start, const FVector& End,
|
||||||
|
int32 Count, bool bIncludeEndpoints = true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates points at fixed distances along a spline.
|
||||||
|
* @param Spline - The spline to sample.
|
||||||
|
* @param Spacing - The distance between regular samples. Must be positive.
|
||||||
|
* @param bIncludeEndpoint - Whether to append the exact end of an open sampling range.
|
||||||
|
* @param SpacingMode - Fixed samples every Spacing units. Even shrinks the spacing so the samples divide the range evenly.
|
||||||
|
* @param CoordinateSpace - The space of the returned points.
|
||||||
|
* @param StartDistance - The distance where sampling starts. Clamped to the spline length.
|
||||||
|
* @param EndDistance - The distance where sampling ends. Negative means the end of the spline.
|
||||||
|
* @returns The generated points, or an empty array for invalid input or an unsupported point count.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Generate Points Along Spline", AdvancedDisplay = "SpacingMode,CoordinateSpace,StartDistance,EndDistance"), Category = "Directive Utilities|Math|Point Generation")
|
||||||
|
static TArray<FVector> GeneratePointsAlongSpline(const USplineComponent* Spline, double Spacing,
|
||||||
|
bool bIncludeEndpoint = true,
|
||||||
|
EDirectiveUtilSplineSpacingMode SpacingMode = EDirectiveUtilSplineSpacingMode::Fixed,
|
||||||
|
ESplineCoordinateSpace::Type CoordinateSpace = ESplineCoordinateSpace::World,
|
||||||
|
double StartDistance = 0.0, double EndDistance = -1.0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a fixed number of evenly spaced points along a spline.
|
||||||
|
* A closed loop spreads the points around the loop; a count of one on an open range returns its midpoint.
|
||||||
|
* @param Count - The number of points to generate.
|
||||||
|
* @param bIncludeEndpoints - Whether the points include both ends of an open sampling range.
|
||||||
|
* @param StartDistance - The distance where sampling starts. Clamped to the spline length.
|
||||||
|
* @param EndDistance - The distance where sampling ends. Negative means the end of the spline.
|
||||||
|
* @returns The generated points, or an empty array for invalid input.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Generate Points Along Spline by Count", AdvancedDisplay = "StartDistance,EndDistance"), Category = "Directive Utilities|Math|Point Generation")
|
||||||
|
static TArray<FVector> GeneratePointsAlongSplineByCount(const USplineComponent* Spline, int32 Count,
|
||||||
|
bool bIncludeEndpoints = true,
|
||||||
|
ESplineCoordinateSpace::Type CoordinateSpace = ESplineCoordinateSpace::World,
|
||||||
|
double StartDistance = 0.0, double EndDistance = -1.0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates transforms at fixed distances along a spline.
|
||||||
|
* Rotation follows the spline tangent and roll. Scale can include the spline scale before applying Scale Multiplier.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Generate Transforms Along Spline", AdvancedDisplay = "SpacingMode,CoordinateSpace,bUseSplineScale,RotationOffset,ScaleMultiplier,StartDistance,EndDistance"), Category = "Directive Utilities|Math|Point Generation")
|
||||||
|
static TArray<FTransform> GenerateTransformsAlongSpline(const USplineComponent* Spline, double Spacing,
|
||||||
|
bool bIncludeEndpoint = true,
|
||||||
|
EDirectiveUtilSplineSpacingMode SpacingMode = EDirectiveUtilSplineSpacingMode::Fixed,
|
||||||
|
ESplineCoordinateSpace::Type CoordinateSpace = ESplineCoordinateSpace::World,
|
||||||
|
bool bUseSplineScale = true,
|
||||||
|
FRotator RotationOffset = FRotator(0.0, 0.0, 0.0), FVector ScaleMultiplier = FVector(1.0, 1.0, 1.0),
|
||||||
|
double StartDistance = 0.0, double EndDistance = -1.0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a fixed number of evenly spaced transforms along a spline.
|
||||||
|
* Rotation follows the spline tangent and roll. Scale can include the spline scale before applying Scale Multiplier.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Generate Transforms Along Spline by Count", AdvancedDisplay = "bUseSplineScale,RotationOffset,ScaleMultiplier,StartDistance,EndDistance"), Category = "Directive Utilities|Math|Point Generation")
|
||||||
|
static TArray<FTransform> GenerateTransformsAlongSplineByCount(const USplineComponent* Spline, int32 Count,
|
||||||
|
bool bIncludeEndpoints = true,
|
||||||
|
ESplineCoordinateSpace::Type CoordinateSpace = ESplineCoordinateSpace::World,
|
||||||
|
bool bUseSplineScale = true,
|
||||||
|
FRotator RotationOffset = FRotator(0.0, 0.0, 0.0), FVector ScaleMultiplier = FVector(1.0, 1.0, 1.0),
|
||||||
|
double StartDistance = 0.0, double EndDistance = -1.0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates evenly spaced points around a circle on the rotated local XY plane.
|
||||||
|
* @returns The generated points without repeating the first point, or an empty array for invalid input.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Generate Points On Circle"), Category = "Directive Utilities|Math|Point Generation")
|
||||||
|
static TArray<FVector> GeneratePointsOnCircle(const FVector& Center, const FRotator& Rotation,
|
||||||
|
double Radius, int32 Count, double StartAngleDegrees = 0.0);
|
||||||
|
|
||||||
|
/** Generates transforms around a circle with fixed, radial, or path-relative orientation. */
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Generate Transforms On Circle", AdvancedDisplay = "RotationOffset,Scale"), Category = "Directive Utilities|Math|Point Generation")
|
||||||
|
static TArray<FTransform> GenerateTransformsOnCircle(const FVector& Center, const FRotator& Rotation,
|
||||||
|
double Radius, int32 Count, double StartAngleDegrees = 0.0,
|
||||||
|
EDirectiveUtilRadialOrientation Orientation = EDirectiveUtilRadialOrientation::FaceCenter,
|
||||||
|
FRotator RotationOffset = FRotator(0.0, 0.0, 0.0), FVector Scale = FVector(1.0, 1.0, 1.0));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates evenly spaced points along an arc on the rotated local XY plane.
|
||||||
|
* @param bIncludeEndpoint - Whether the final point lies at Start Angle plus Arc Angle.
|
||||||
|
* @returns The generated points, or an empty array for invalid input.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Generate Points On Arc"), Category = "Directive Utilities|Math|Point Generation")
|
||||||
|
static TArray<FVector> GeneratePointsOnArc(const FVector& Center, const FRotator& Rotation,
|
||||||
|
double Radius, int32 Count, double StartAngleDegrees = 0.0, double ArcAngleDegrees = 90.0,
|
||||||
|
bool bIncludeEndpoint = true);
|
||||||
|
|
||||||
|
/** Generates transforms along an arc with fixed, radial, or path-relative orientation. */
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Generate Transforms On Arc", AdvancedDisplay = "RotationOffset,Scale"), Category = "Directive Utilities|Math|Point Generation")
|
||||||
|
static TArray<FTransform> GenerateTransformsOnArc(const FVector& Center, const FRotator& Rotation,
|
||||||
|
double Radius, int32 Count, double StartAngleDegrees = 0.0, double ArcAngleDegrees = 90.0,
|
||||||
|
bool bIncludeEndpoint = true,
|
||||||
|
EDirectiveUtilRadialOrientation Orientation = EDirectiveUtilRadialOrientation::FaceCenter,
|
||||||
|
FRotator RotationOffset = FRotator(0.0, 0.0, 0.0), FVector Scale = FVector(1.0, 1.0, 1.0));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a deterministic sunflower distribution across a disc on the rotated local XY plane.
|
||||||
|
* @returns Approximately even area coverage, or an empty array for invalid input.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Generate Points On Disc"), Category = "Directive Utilities|Math|Point Generation")
|
||||||
|
static TArray<FVector> GeneratePointsOnDisc(const FVector& Center, const FRotator& Rotation,
|
||||||
|
double Radius, int32 Count, double AngleOffsetDegrees = 0.0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a deterministic Fibonacci distribution across a sphere surface.
|
||||||
|
* @returns Approximately even surface coverage, or an empty array for invalid input.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Generate Points On Sphere"), Category = "Directive Utilities|Math|Point Generation")
|
||||||
|
static TArray<FVector> GeneratePointsOnSphere(const FVector& Center, const FRotator& Rotation,
|
||||||
|
double Radius, int32 Count, double AngleOffsetDegrees = 0.0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offsets each location along a direction by Perlin noise sampled at that location.
|
||||||
|
* The offset varies smoothly between -Amplitude and Amplitude across the noise field.
|
||||||
|
* @param Locations - The locations to offset.
|
||||||
|
* @param NoiseScale - The world-space size of the noise features. Must be positive.
|
||||||
|
* @param Amplitude - The maximum offset distance along the direction.
|
||||||
|
* @param Direction - The offset direction. Its magnitude is ignored.
|
||||||
|
* @param NoiseOffset - World-space shift of the noise field, for varying the pattern between layers.
|
||||||
|
* @returns The offset locations, or an empty array for invalid input.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Offset Locations By Noise", BlueprintThreadSafe, AdvancedDisplay = "Direction,NoiseOffset"), Category = "Directive Utilities|Math|Point Generation")
|
||||||
|
static TArray<FVector> OffsetLocationsByNoise(const TArray<FVector>& Locations, double NoiseScale,
|
||||||
|
double Amplitude, FVector Direction = FVector(0.0, 0.0, 1.0),
|
||||||
|
FVector NoiseOffset = FVector(0.0, 0.0, 0.0));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offsets each transform location along a direction by Perlin noise sampled at that location.
|
||||||
|
* Rotation and scale are unchanged. Behaves like Offset Locations By Noise.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Offset Transforms By Noise", BlueprintThreadSafe, AdvancedDisplay = "Direction,NoiseOffset"), Category = "Directive Utilities|Math|Point Generation")
|
||||||
|
static TArray<FTransform> OffsetTransformsByNoise(const TArray<FTransform>& Transforms, double NoiseScale,
|
||||||
|
double Amplitude, FVector Direction = FVector(0.0, 0.0, 1.0),
|
||||||
|
FVector NoiseOffset = FVector(0.0, 0.0, 0.0));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies a Back/Elastic/Bounce easing curve to a normalized alpha.
|
* 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.
|
* @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.
|
* 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 Alpha - The input alpha. Clamped to the [0, 1] range.
|
||||||
* @param EaseType - The easing curve to apply.
|
* @param EaseType - The easing curve to apply.
|
||||||
* @returns The eased alpha. Note that Back and Elastic curves intentionally overshoot the [0, 1] range.
|
* @returns The eased alpha. Endpoints are exact. Back and Elastic curves intentionally overshoot the [0, 1] range between the endpoints.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintPure, meta = (DisplayName = "Ease Alpha", BlueprintThreadSafe), Category = "Directive Utilities|Math|Easing")
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Ease Alpha", BlueprintThreadSafe), Category = "Directive Utilities|Math|Easing")
|
||||||
static float EaseAlpha(float Alpha, EDirectiveUtilEaseType EaseType);
|
static float EaseAlpha(float Alpha, EDirectiveUtilEaseType EaseType);
|
||||||
@@ -101,6 +592,39 @@ public:
|
|||||||
UFUNCTION(BlueprintPure, meta = (DisplayName = "Ease (Color)", BlueprintThreadSafe), Category = "Directive Utilities|Math|Easing")
|
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);
|
static FLinearColor EaseColor(const FLinearColor& A, const FLinearColor& B, float Alpha, EDirectiveUtilEaseType EaseType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eases a transform from A to B. Rotation takes the shortest path; location and scale interpolate linearly
|
||||||
|
* before the eased alpha is applied.
|
||||||
|
* @param A - The start transform (returned at Alpha 0).
|
||||||
|
* @param B - The target transform (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 transform between A and B.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Ease (Transform)", BlueprintThreadSafe), Category = "Directive Utilities|Math|Easing")
|
||||||
|
static FTransform EaseTransform(const FTransform& A, const FTransform& B, float Alpha, EDirectiveUtilEaseType EaseType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eases each location in From toward the same index in To. Use with two generated layouts to blend formations.
|
||||||
|
* @param Alpha - The shared input alpha. Clamped to the [0, 1] range.
|
||||||
|
* @param PerElementAlphas - When non-empty, one alpha per element replaces Alpha for staggered blends.
|
||||||
|
* @returns The eased locations, or an empty array for mismatched lengths or non-finite input.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Ease Location Arrays", AutoCreateRefTerm = "PerElementAlphas", BlueprintThreadSafe, AdvancedDisplay = "PerElementAlphas"), Category = "Directive Utilities|Math|Easing")
|
||||||
|
static TArray<FVector> EaseLocationArrays(const TArray<FVector>& From, const TArray<FVector>& To,
|
||||||
|
float Alpha, EDirectiveUtilEaseType EaseType, const TArray<float>& PerElementAlphas);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eases each transform in From toward the same index in To. Use with two generated layouts to blend formations.
|
||||||
|
* Rotation takes the shortest path; location and scale interpolate linearly before the eased alpha is applied.
|
||||||
|
* @param Alpha - The shared input alpha. Clamped to the [0, 1] range.
|
||||||
|
* @param PerElementAlphas - When non-empty, one alpha per element replaces Alpha for staggered blends.
|
||||||
|
* @returns The eased transforms, or an empty array for mismatched lengths or non-finite input.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Ease Transform Arrays", AutoCreateRefTerm = "PerElementAlphas", BlueprintThreadSafe, AdvancedDisplay = "PerElementAlphas"), Category = "Directive Utilities|Math|Easing")
|
||||||
|
static TArray<FTransform> EaseTransformArrays(const TArray<FTransform>& From, const TArray<FTransform>& To,
|
||||||
|
float Alpha, EDirectiveUtilEaseType EaseType, const TArray<float>& PerElementAlphas);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rounds a float to a given number of decimal places. Rounds half away from zero,
|
* Rounds a float to a given number of decimal places. Rounds half away from zero,
|
||||||
* matching "Round To Decimals (Text)".
|
* matching "Round To Decimals (Text)".
|
||||||
@@ -138,7 +662,7 @@ public:
|
|||||||
* Formats a duration in seconds as d/h/m/s units from the largest nonzero unit down, with
|
* 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
|
* 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
|
* 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.
|
* a leading minus sign when a nonzero unit remains; non-finite input returns "0s". Output is English-only.
|
||||||
* @param Seconds - The duration in seconds.
|
* @param Seconds - The duration in seconds.
|
||||||
* @param bIncludeSeconds - Whether to include the seconds unit.
|
* @param bIncludeSeconds - Whether to include the seconds unit.
|
||||||
* @returns The formatted duration text.
|
* @returns The formatted duration text.
|
||||||
@@ -224,9 +748,59 @@ public:
|
|||||||
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
|
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
|
||||||
static float GetFloatArrayStandardDeviation(const TArray<float>& Values);
|
static float GetFloatArrayStandardDeviation(const TArray<float>& Values);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the circular mean of an angle array in degrees.
|
||||||
|
* @returns False for an empty array, non-finite input, or an undefined or numerically indeterminate circular mean.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Angle Array Average", BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
|
||||||
|
static bool GetAngleArrayAverage(const TArray<float>& Angles, float& AverageAngle, float& ResultantStrength);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the weighted average of a float array.
|
||||||
|
* @returns False when the arrays differ in size, contain invalid values, or have no positive weight.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Weighted Float Array Average", BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
|
||||||
|
static bool GetWeightedFloatArrayAverage(const TArray<float>& Values, const TArray<float>& Weights, float& Average);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the weighted average of a vector array.
|
||||||
|
* @returns False when the arrays differ in size, contain invalid values, or have no positive weight.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Weighted Vector Array Average", BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
|
||||||
|
static bool GetWeightedVectorArrayAverage(const TArray<FVector>& Values, const TArray<float>& Weights, FVector& Average);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a float array to an output range.
|
||||||
|
* @returns False for an empty array or non-finite input.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Normalize Float Array To Range", BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
|
||||||
|
static bool NormalizeFloatArrayToRange(const TArray<float>& Values, float OutputMinimum, float OutputMaximum,
|
||||||
|
TArray<float>& NormalizedValues);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes positive weights so their sum is one. Negative and non-finite weights are treated as zero.
|
||||||
|
* @returns False for an empty array or when no positive weight remains.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Normalize Weights", BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
|
||||||
|
static bool NormalizeWeights(const TArray<float>& Weights, TArray<float>& NormalizedWeights);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates a percentile using the Type 7 linear method without modifying the input array.
|
||||||
|
* @returns False for an empty array or non-finite input.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Float Array Percentile", BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
|
||||||
|
static bool GetFloatArrayPercentile(const TArray<float>& Values, float Percentile, float& Value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the root mean square of a float array.
|
||||||
|
* @returns False for an empty array or non-finite input.
|
||||||
|
*/
|
||||||
|
UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Float Array Root Mean Square", BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
|
||||||
|
static bool GetFloatArrayRootMeanSquare(const TArray<float>& Values, float& RootMeanSquare);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a random index into the Weights array, where each index's probability is proportional to its weight.
|
* 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.
|
* Useful for loot tables and weighted spawning. Negative and non-finite weights are treated as zero.
|
||||||
* @param Weights - The per-index weights.
|
* @param Weights - The per-index weights.
|
||||||
* @returns The selected index, or INDEX_NONE (-1) if the array is empty or all weights are zero.
|
* @returns The selected index, or INDEX_NONE (-1) if the array is empty or all weights are zero.
|
||||||
*/
|
*/
|
||||||
@@ -236,9 +810,33 @@ public:
|
|||||||
/**
|
/**
|
||||||
* Deterministic version of Get Random Index From Weights that draws from (and advances) the provided random stream.
|
* 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 Stream - The random stream to draw from.
|
||||||
* @param Weights - The per-index weights. Negative weights are treated as zero.
|
* @param Weights - The per-index weights. Negative and non-finite weights are treated as zero.
|
||||||
* @returns The selected index, or INDEX_NONE (-1) if the array is empty or all weights are 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")
|
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);
|
static int32 GetRandomIndexFromWeightsFromStream(UPARAM(ref) FRandomStream& Stream, const TArray<float>& Weights);
|
||||||
|
|
||||||
|
/** Returns a uniformly distributed random point inside a circle. */
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Random Point In Circle"), Category = "Directive Utilities|Math|Random")
|
||||||
|
static FVector2D RandomPointInCircle(float Radius);
|
||||||
|
|
||||||
|
/** Returns a deterministic uniformly distributed random point inside a circle. Invalid or zero radii do not advance the stream. */
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Random Point In Circle (Stream)"), Category = "Directive Utilities|Math|Random")
|
||||||
|
static FVector2D RandomPointInCircleFromStream(UPARAM(ref) FRandomStream& Stream, float Radius);
|
||||||
|
|
||||||
|
/** Returns a uniformly distributed random point inside a 2D annulus. */
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Random Point In Annulus"), Category = "Directive Utilities|Math|Random")
|
||||||
|
static FVector2D RandomPointInAnnulus(float InnerRadius, float OuterRadius);
|
||||||
|
|
||||||
|
/** Returns a deterministic uniformly distributed random point inside a 2D annulus. Invalid or zero radii do not advance the stream. */
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Random Point In Annulus (Stream)"), Category = "Directive Utilities|Math|Random")
|
||||||
|
static FVector2D RandomPointInAnnulusFromStream(UPARAM(ref) FRandomStream& Stream, float InnerRadius, float OuterRadius);
|
||||||
|
|
||||||
|
/** Returns a uniformly distributed random point inside a sphere. */
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Random Point In Sphere"), Category = "Directive Utilities|Math|Random")
|
||||||
|
static FVector RandomPointInSphere(float Radius);
|
||||||
|
|
||||||
|
/** Returns a deterministic uniformly distributed random point inside a sphere. Invalid or zero radii do not advance the stream. */
|
||||||
|
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Random Point In Sphere (Stream)"), Category = "Directive Utilities|Math|Random")
|
||||||
|
static FVector RandomPointInSphereFromStream(UPARAM(ref) FRandomStream& Stream, float Radius);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class USaveGame;
|
|||||||
* Save-slot utilities that fill the gaps left by UGameplayStatics: enumerating slots, reading slot
|
* 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,
|
* 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.
|
* not a save framework: use the engine's SaveGameToSlot/LoadGameFromSlot for the actual slot I/O.
|
||||||
|
* Slot operations accept flat file names so they behave consistently across platform save backends.
|
||||||
*/
|
*/
|
||||||
UCLASS()
|
UCLASS()
|
||||||
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilSaveGameFunctionLibrary : public UBlueprintFunctionLibrary
|
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilSaveGameFunctionLibrary : public UBlueprintFunctionLibrary
|
||||||
@@ -22,9 +23,10 @@ class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilSaveGameFunctionLibrary : publ
|
|||||||
public:
|
public:
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the names of all existing save slots in the project's default save directory.
|
* Returns the names of all existing save slots known to the engine save game system.
|
||||||
* @note This enumerates the engine's default file-based save directory (Saved/SaveGames); it does not
|
* Uses ISaveGameSystem::GetSaveGameNames so the result matches DoesSaveSlotExist /
|
||||||
* cover platform-specific save systems (e.g. console storage).
|
* DeleteSaveSlot / RenameSaveSlot. Falls back to the default Saved/SaveGames directory
|
||||||
|
* only when the active backend cannot enumerate slots.
|
||||||
* @returns The save slot names (without extension).
|
* @returns The save slot names (without extension).
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|SaveGame")
|
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|SaveGame")
|
||||||
@@ -34,7 +36,7 @@ public:
|
|||||||
* Returns the last-modified timestamp of a save slot, if it exists.
|
* Returns the last-modified timestamp of a save slot, if it exists.
|
||||||
* @param SlotName - The save slot name.
|
* @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.
|
* @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.
|
* Converted from the file system's UTC timestamp using the timezone rules for that instant.
|
||||||
* @returns True if the slot exists.
|
* @returns True if the slot exists.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|SaveGame")
|
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|SaveGame")
|
||||||
@@ -81,8 +83,9 @@ public:
|
|||||||
/**
|
/**
|
||||||
* Renames a save slot by copying its data to the new name and then deleting the original.
|
* 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
|
* 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
|
* exists, and the new slot does not. Case-only renames (Slot -> slot) rewrite the existing
|
||||||
* the engine's save game system, so unlike enumeration it also works on platform save backends.
|
* slot instead of reporting a collision. On failure the original slot is never lost. Goes through
|
||||||
|
* the engine's save game system so it stays consistent with DoesSaveSlotExist and GetAllSaveSlotNames.
|
||||||
* @param OldSlotName - The existing save slot name.
|
* @param OldSlotName - The existing save slot name.
|
||||||
* @param NewSlotName - The new save slot name.
|
* @param NewSlotName - The new save slot name.
|
||||||
* @param UserIndex - The platform user index the save belongs to.
|
* @param UserIndex - The platform user index the save belongs to.
|
||||||
|
|||||||
@@ -305,7 +305,8 @@ public:
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether the string is safe to use as a bare file name: not empty, no path
|
* 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.
|
* separators or relative segments, no characters invalid in file names, no trailing
|
||||||
|
* dot or space, and not a reserved device name (CON, PRN, AUX, NUL, COM1-9, LPT1-9).
|
||||||
* @param String - The string to check.
|
* @param String - The string to check.
|
||||||
* @returns True if the string is a valid bare file name.
|
* @returns True if the string is a valid bare file name.
|
||||||
*/
|
*/
|
||||||
@@ -314,7 +315,8 @@ public:
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the string with path separators and characters invalid in file names removed
|
* 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.
|
* (or replaced when a replacement character is provided). Trailing dots and spaces are
|
||||||
|
* stripped. Reserved device names are prefixed with an underscore. May return an empty string.
|
||||||
* @param String - The string to sanitize.
|
* @param String - The string to sanitize.
|
||||||
* @param Replacement - Optional single-character replacement for stripped characters.
|
* @param Replacement - Optional single-character replacement for stripped characters.
|
||||||
* @returns The sanitized file name.
|
* @returns The sanitized file name.
|
||||||
|
|||||||
@@ -21,6 +21,6 @@ public:
|
|||||||
* Returns true if the provided text is not empty.
|
* Returns true if the provided text is not empty.
|
||||||
* @param Text - The text to check.
|
* @param Text - The text to check.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Text" )
|
UFUNCTION(BlueprintPure, meta = (AutoCreateRefTerm = "Text"), Category = "Directive Utilities|Text")
|
||||||
static bool IsNotEmpty(const FText& Text);
|
static bool IsNotEmpty(const FText& Text);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Engine/CancellableAsyncAction.h"
|
||||||
|
#include "Kismet/BlueprintAsyncActionBase.h"
|
||||||
|
#include "DirectiveUtilAsyncActionBase.generated.h"
|
||||||
|
|
||||||
|
UCLASS(Abstract)
|
||||||
|
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilAsyncActionBase : public UBlueprintAsyncActionBase
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual void RegisterWithGameInstance(const UObject* WorldContextObject) override;
|
||||||
|
virtual void SetReadyToDestroy() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void BeginDestroy() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void HandleWorldCleanup(UWorld* World, bool bSessionEnded, bool bCleanupResources);
|
||||||
|
void UnbindWorldCleanup();
|
||||||
|
|
||||||
|
TWeakObjectPtr<UWorld> RegisteredWorld;
|
||||||
|
FDelegateHandle WorldCleanupHandle;
|
||||||
|
};
|
||||||
|
|
||||||
|
UCLASS(Abstract)
|
||||||
|
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilCancellableAsyncAction : public UCancellableAsyncAction
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual void RegisterWithGameInstance(const UObject* WorldContextObject) override;
|
||||||
|
virtual void SetReadyToDestroy() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual void BeginDestroy() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void HandleWorldCleanup(UWorld* World, bool bSessionEnded, bool bCleanupResources);
|
||||||
|
void UnbindWorldCleanup();
|
||||||
|
|
||||||
|
TWeakObjectPtr<UWorld> RegisteredWorld;
|
||||||
|
FDelegateHandle WorldCleanupHandle;
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "Kismet/BlueprintAsyncActionBase.h"
|
#include "Tasks/DirectiveUtilAsyncActionBase.h"
|
||||||
#include "UObject/SoftObjectPtr.h"
|
#include "UObject/SoftObjectPtr.h"
|
||||||
#include "DirectiveUtilTask_AsyncLoadAsset.generated.h"
|
#include "DirectiveUtilTask_AsyncLoadAsset.generated.h"
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnAsyncLoadAssetsProgress, int32,
|
|||||||
* Asynchronously loads a soft object reference and broadcasts the loaded asset, with a cancel option.
|
* Asynchronously loads a soft object reference and broadcasts the loaded asset, with a cancel option.
|
||||||
*/
|
*/
|
||||||
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Load Asset"))
|
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Load Asset"))
|
||||||
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_AsyncLoadAsset : public UBlueprintAsyncActionBase
|
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_AsyncLoadAsset : public UDirectiveUtilAsyncActionBase
|
||||||
{
|
{
|
||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ protected:
|
|||||||
* Asynchronously loads a soft class reference and broadcasts the loaded class, with a cancel option.
|
* Asynchronously loads a soft class reference and broadcasts the loaded class, with a cancel option.
|
||||||
*/
|
*/
|
||||||
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Load Class"))
|
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Load Class"))
|
||||||
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_AsyncLoadClass : public UBlueprintAsyncActionBase
|
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_AsyncLoadClass : public UDirectiveUtilAsyncActionBase
|
||||||
{
|
{
|
||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ protected:
|
|||||||
* loaded assets, with progress updates and a cancel option.
|
* loaded assets, with progress updates and a cancel option.
|
||||||
*/
|
*/
|
||||||
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Load Assets"))
|
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Load Assets"))
|
||||||
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_AsyncLoadAssets : public UBlueprintAsyncActionBase
|
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_AsyncLoadAssets : public UDirectiveUtilAsyncActionBase
|
||||||
{
|
{
|
||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "Kismet/BlueprintAsyncActionBase.h"
|
#include "Tasks/DirectiveUtilAsyncActionBase.h"
|
||||||
#include "Engine/EngineTypes.h"
|
#include "Engine/EngineTypes.h"
|
||||||
#include "Engine/HitResult.h"
|
#include "Engine/HitResult.h"
|
||||||
#include "DirectiveUtilTask_AsyncTrace.generated.h"
|
#include "DirectiveUtilTask_AsyncTrace.generated.h"
|
||||||
@@ -28,7 +28,7 @@ enum class EDirectiveUtilTraceShape : uint8
|
|||||||
* The trace cannot be cancelled.
|
* The trace cannot be cancelled.
|
||||||
*/
|
*/
|
||||||
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Trace"))
|
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Trace"))
|
||||||
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_AsyncTrace : public UBlueprintAsyncActionBase
|
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_AsyncTrace : public UDirectiveUtilAsyncActionBase
|
||||||
{
|
{
|
||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "Kismet/BlueprintAsyncActionBase.h"
|
#include "Tasks/DirectiveUtilAsyncActionBase.h"
|
||||||
#include "Engine/TimerHandle.h"
|
#include "Engine/TimerHandle.h"
|
||||||
#include "DirectiveUtilTask_Delay.generated.h"
|
#include "DirectiveUtilTask_Delay.generated.h"
|
||||||
|
|
||||||
@@ -11,10 +11,10 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnDelayCompleted);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* DirectiveUtilTask_Delay
|
* DirectiveUtilTask_Delay
|
||||||
* A cancellable delay that can be ended early by calling EndTask.
|
* A cancellable delay.
|
||||||
*/
|
*/
|
||||||
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Cancellable Delay"))
|
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Cancellable Delay"))
|
||||||
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_Delay : public UBlueprintAsyncActionBase
|
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_Delay : public UDirectiveUtilCancellableAsyncAction
|
||||||
{
|
{
|
||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
@@ -23,10 +23,10 @@ public:
|
|||||||
/**
|
/**
|
||||||
* Starts a cancellable delay.
|
* Starts a cancellable delay.
|
||||||
* When the delay has completed, the Completed delegate is called.
|
* When the delay has completed, the Completed delegate is called.
|
||||||
* Call EndTask to cancel the delay before it completes.
|
* Call Cancel to stop the delay before it completes.
|
||||||
*
|
*
|
||||||
* @param WorldContextObject The world context object.
|
* @param WorldContextObject The world context object.
|
||||||
* @param Duration The duration of the delay in seconds.
|
* @param Duration The duration of the delay in seconds. Non-finite and non-positive values complete on the next timer tick.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(
|
UFUNCTION(
|
||||||
BlueprintCallable,
|
BlueprintCallable,
|
||||||
@@ -38,14 +38,13 @@ public:
|
|||||||
))
|
))
|
||||||
static UDirectiveUtilTask_Delay* CancellableDelay(UObject* WorldContextObject, float Duration);
|
static UDirectiveUtilTask_Delay* CancellableDelay(UObject* WorldContextObject, float Duration);
|
||||||
|
|
||||||
/**
|
UFUNCTION(BlueprintCallable, meta=(DeprecatedFunction, DeprecationMessage="Use Cancel instead."), Category = "Directive Utilities|FlowControl")
|
||||||
* Ends the delay early.
|
|
||||||
*/
|
|
||||||
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|FlowControl")
|
|
||||||
void EndTask();
|
void EndTask();
|
||||||
virtual void Activate() override;
|
virtual void Activate() override;
|
||||||
|
virtual void Cancel() override;
|
||||||
|
virtual bool IsActive() const override;
|
||||||
|
virtual bool ShouldBroadcastDelegates() const override;
|
||||||
|
|
||||||
// The delegate called when the delay has completed.
|
|
||||||
UPROPERTY(BlueprintAssignable)
|
UPROPERTY(BlueprintAssignable)
|
||||||
FOnDelayCompleted Completed;
|
FOnDelayCompleted Completed;
|
||||||
|
|
||||||
@@ -55,6 +54,7 @@ protected:
|
|||||||
TObjectPtr<UObject> WorldContextObject;
|
TObjectPtr<UObject> WorldContextObject;
|
||||||
|
|
||||||
float Duration = 0.0f;
|
float Duration = 0.0f;
|
||||||
|
bool bFinished = false;
|
||||||
FTimerHandle TimerHandle;
|
FTimerHandle TimerHandle;
|
||||||
|
|
||||||
void OnDelayComplete();
|
void OnDelayComplete();
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "Tasks/DirectiveUtilAsyncActionBase.h"
|
||||||
|
#include "Engine/TimerHandle.h"
|
||||||
|
#include "DirectiveUtilTask_Flow.generated.h"
|
||||||
|
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnDurationUpdated, float, ElapsedTime, float, DeltaTime, float, Alpha);
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnDurationCompleted);
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnRepeatIteration, int32, Index, int32, Remaining);
|
||||||
|
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnRepeatCompleted);
|
||||||
|
|
||||||
|
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Update for Duration"))
|
||||||
|
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_UpdateForDuration : public UDirectiveUtilCancellableAsyncAction
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UFUNCTION(
|
||||||
|
BlueprintCallable,
|
||||||
|
meta=(
|
||||||
|
BlueprintInternalUseOnly = "true",
|
||||||
|
Category = "Directive Utilities|FlowControl",
|
||||||
|
WorldContext = "WorldContextObject",
|
||||||
|
DisplayName = "Update for Duration",
|
||||||
|
AdvancedDisplay = "UpdateInterval"
|
||||||
|
))
|
||||||
|
static UDirectiveUtilTask_UpdateForDuration* UpdateForDuration(
|
||||||
|
UObject* WorldContextObject,
|
||||||
|
float Duration,
|
||||||
|
float UpdateInterval = 0.0f);
|
||||||
|
|
||||||
|
virtual void Activate() override;
|
||||||
|
virtual void Cancel() override;
|
||||||
|
virtual bool IsActive() const override;
|
||||||
|
virtual bool ShouldBroadcastDelegates() const override;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable)
|
||||||
|
FOnDurationUpdated Updated;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable)
|
||||||
|
FOnDurationCompleted Completed;
|
||||||
|
|
||||||
|
private:
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UObject> WorldContextObject;
|
||||||
|
|
||||||
|
float Duration = 0.0f;
|
||||||
|
float UpdateInterval = 0.0f;
|
||||||
|
float LastElapsedTime = 0.0f;
|
||||||
|
bool bHasUpdated = false;
|
||||||
|
bool bFinished = false;
|
||||||
|
FTimerHandle UpdateTimerHandle;
|
||||||
|
FTimerHandle CompletionTimerHandle;
|
||||||
|
|
||||||
|
void OnUpdate();
|
||||||
|
void OnComplete();
|
||||||
|
void BroadcastUpdate(float ElapsedTime, float Alpha);
|
||||||
|
void ClearTimers();
|
||||||
|
};
|
||||||
|
|
||||||
|
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Repeat with Interval"))
|
||||||
|
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_RepeatWithInterval : public UDirectiveUtilCancellableAsyncAction
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* Runs a fixed number of iterations, or forever when Count is -1.
|
||||||
|
* @param Count - Iteration count. Use -1 to repeat until Cancel. Zero and other negative values complete on the next tick with no iterations.
|
||||||
|
* @param Interval - Delay between iterations. Non-positive or non-finite values run on consecutive ticks.
|
||||||
|
* @param InitialDelay - Delay before the first iteration. Non-positive or non-finite values start on the next tick.
|
||||||
|
*/
|
||||||
|
UFUNCTION(
|
||||||
|
BlueprintCallable,
|
||||||
|
meta=(
|
||||||
|
BlueprintInternalUseOnly = "true",
|
||||||
|
Category = "Directive Utilities|FlowControl",
|
||||||
|
WorldContext = "WorldContextObject",
|
||||||
|
DisplayName = "Repeat with Interval",
|
||||||
|
AdvancedDisplay = "InitialDelay"
|
||||||
|
))
|
||||||
|
static UDirectiveUtilTask_RepeatWithInterval* RepeatWithInterval(
|
||||||
|
UObject* WorldContextObject,
|
||||||
|
int32 Count,
|
||||||
|
float Interval,
|
||||||
|
float InitialDelay = 0.0f);
|
||||||
|
|
||||||
|
virtual void Activate() override;
|
||||||
|
virtual void Cancel() override;
|
||||||
|
virtual bool IsActive() const override;
|
||||||
|
virtual bool ShouldBroadcastDelegates() const override;
|
||||||
|
|
||||||
|
#if WITH_DEV_AUTOMATION_TESTS
|
||||||
|
void SetNextIndexForTesting(int32 Index) { NextIndex = Index; }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable)
|
||||||
|
FOnRepeatIteration Iteration;
|
||||||
|
|
||||||
|
UPROPERTY(BlueprintAssignable)
|
||||||
|
FOnRepeatCompleted Completed;
|
||||||
|
|
||||||
|
private:
|
||||||
|
UPROPERTY()
|
||||||
|
TObjectPtr<UObject> WorldContextObject;
|
||||||
|
|
||||||
|
int32 Count = 0;
|
||||||
|
int32 NextIndex = 0;
|
||||||
|
float Interval = 0.0f;
|
||||||
|
float InitialDelay = 0.0f;
|
||||||
|
bool bFinished = false;
|
||||||
|
FTimerHandle TimerHandle;
|
||||||
|
|
||||||
|
void Schedule(float Delay);
|
||||||
|
void OnIteration();
|
||||||
|
void Complete();
|
||||||
|
void ClearTimer();
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "Kismet/BlueprintAsyncActionBase.h"
|
#include "Tasks/DirectiveUtilAsyncActionBase.h"
|
||||||
#include "GameFramework/Controller.h"
|
#include "GameFramework/Controller.h"
|
||||||
#include "DirectiveUtilTask_MoveToLocation.generated.h"
|
#include "DirectiveUtilTask_MoveToLocation.generated.h"
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAsyncMoveToActor, bool, bSuccess)
|
|||||||
* Asynchronously moves an actor to a location.
|
* Asynchronously moves an actor to a location.
|
||||||
*/
|
*/
|
||||||
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Move To Location"))
|
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Move To Location"))
|
||||||
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_MoveToLocation : public UBlueprintAsyncActionBase
|
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_MoveToLocation : public UDirectiveUtilAsyncActionBase
|
||||||
{
|
{
|
||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
@@ -27,15 +27,15 @@ public:
|
|||||||
* bSuccess is true only when the pawn ends within AcceptanceRadius of Destination; the task also ends
|
* 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.
|
* (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 the controller, pawn, or world becomes unavailable while moving, the task will automatically end.
|
||||||
* If bCheckStuckMovement is enabled and the controller gets stuck 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 WorldContextObject The world context object.
|
||||||
* @param Controller The controller to move.
|
* @param Controller The controller to move.
|
||||||
* @param Destination The vector location to move to.
|
* @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 AcceptanceRadius The radius around the destination location that is considered acceptable. Negative and non-finite values are treated as zero.
|
||||||
* @param bCheckStuckMovement Check if the controller gets stuck while moving.
|
* @param bCheckStuckMovement Check if the controller gets stuck while moving.
|
||||||
* @param StuckThreshold The distance threshold to consider the controller stuck.
|
* @param StuckThreshold The distance threshold to consider the controller stuck. Negative and non-finite values are treated as zero.
|
||||||
* @param bDebugLineTrace Display a line trace to the destination location for a short duration.
|
* @param bDebugLineTrace Display a line trace to the destination location for a short duration.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(
|
UFUNCTION(
|
||||||
@@ -86,6 +86,8 @@ protected:
|
|||||||
|
|
||||||
FTimerHandle StuckTimerHandle;
|
FTimerHandle StuckTimerHandle;
|
||||||
|
|
||||||
|
TWeakObjectPtr<UWorld> TimerWorld;
|
||||||
|
|
||||||
bool bHasCompleted = false;
|
bool bHasCompleted = false;
|
||||||
|
|
||||||
void CheckMoveToLocation();
|
void CheckMoveToLocation();
|
||||||
@@ -100,7 +102,7 @@ protected:
|
|||||||
* Asynchronously moves an actor to another actor.
|
* Asynchronously moves an actor to another actor.
|
||||||
*/
|
*/
|
||||||
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Move To Actor"))
|
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Move To Actor"))
|
||||||
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_MoveToActor : public UBlueprintAsyncActionBase
|
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_MoveToActor : public UDirectiveUtilAsyncActionBase
|
||||||
{
|
{
|
||||||
GENERATED_BODY()
|
GENERATED_BODY()
|
||||||
|
|
||||||
@@ -113,15 +115,15 @@ public:
|
|||||||
* (with the same distance test) when path-following stops for any reason. The goal's location is re-read
|
* (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.
|
* 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 the controller, pawn, goal actor, or world becomes unavailable while moving, the task will automatically end.
|
||||||
* If bCheckStuckMovement is enabled and the controller gets stuck 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 WorldContextObject The world context object.
|
||||||
* @param Controller The controller to move.
|
* @param Controller The controller to move.
|
||||||
* @param Goal The actor to move to.
|
* @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 AcceptanceRadius The radius around the goal actor that is considered acceptable. Negative and non-finite values are treated as zero.
|
||||||
* @param bCheckStuckMovement Check if the controller gets stuck while moving.
|
* @param bCheckStuckMovement Check if the controller gets stuck while moving.
|
||||||
* @param StuckThreshold The distance threshold to consider the controller stuck.
|
* @param StuckThreshold The distance threshold to consider the controller stuck. Negative and non-finite values are treated as zero.
|
||||||
*/
|
*/
|
||||||
UFUNCTION(
|
UFUNCTION(
|
||||||
BlueprintCallable,
|
BlueprintCallable,
|
||||||
@@ -171,6 +173,8 @@ protected:
|
|||||||
|
|
||||||
FTimerHandle StuckTimerHandle;
|
FTimerHandle StuckTimerHandle;
|
||||||
|
|
||||||
|
TWeakObjectPtr<UWorld> TimerWorld;
|
||||||
|
|
||||||
bool bHasCompleted = false;
|
bool bHasCompleted = false;
|
||||||
|
|
||||||
void CheckMoveToActor();
|
void CheckMoveToActor();
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#pragma once
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "DirectiveUtilInputTypes.generated.h"
|
#include "DirectiveUtilInputTypes.generated.h"
|
||||||
|
|||||||
@@ -5,8 +5,37 @@
|
|||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "DirectiveUtilMathTypes.generated.h"
|
#include "DirectiveUtilMathTypes.generated.h"
|
||||||
|
|
||||||
|
/** Hex tile orientation on the local XY plane. */
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EDirectiveUtilHexOrientation : uint8
|
||||||
|
{
|
||||||
|
PointyTop UMETA(DisplayName = "Pointy Top"),
|
||||||
|
FlatTop UMETA(DisplayName = "Flat Top"),
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Rotation applied to transforms generated around a center point. */
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EDirectiveUtilRadialOrientation : uint8
|
||||||
|
{
|
||||||
|
Fixed UMETA(DisplayName = "Fixed"),
|
||||||
|
FaceCenter UMETA(DisplayName = "Face Center"),
|
||||||
|
FaceAwayFromCenter UMETA(DisplayName = "Face Away From Center"),
|
||||||
|
FollowPath UMETA(DisplayName = "Follow Path"),
|
||||||
|
FaceAgainstPath UMETA(DisplayName = "Face Against Path"),
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Spacing behavior for spline sample generation. */
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EDirectiveUtilSplineSpacingMode : uint8
|
||||||
|
{
|
||||||
|
Fixed UMETA(DisplayName = "Fixed", Tooltip = "Samples every Spacing units. The final interval may be shorter."),
|
||||||
|
Even UMETA(DisplayName = "Even", Tooltip = "Shrinks Spacing so the samples divide the range evenly."),
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Easing curves not provided by the engine's built-in Ease node (EEasingFunc): the classic Penner Back, Elastic and Bounce curves.
|
* Easing curves not provided by the engine's built-in Ease node (EEasingFunc): the classic Penner Back, Elastic and Bounce curves,
|
||||||
|
* plus Linear for un-eased interpolation.
|
||||||
|
* New values append at the end so existing Blueprint ordinals stay stable.
|
||||||
*/
|
*/
|
||||||
UENUM(BlueprintType)
|
UENUM(BlueprintType)
|
||||||
enum class EDirectiveUtilEaseType : uint8
|
enum class EDirectiveUtilEaseType : uint8
|
||||||
@@ -20,4 +49,5 @@ enum class EDirectiveUtilEaseType : uint8
|
|||||||
BounceIn UMETA(DisplayName = "Bounce In", Tooltip="Bounces with increasing energy before easing in."),
|
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."),
|
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."),
|
BounceInOut UMETA(DisplayName = "Bounce In Out", Tooltip="Bounces at both the start and the end."),
|
||||||
|
Linear UMETA(DisplayName = "Linear", Tooltip="Interpolates at a constant rate without easing."),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#pragma once
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "DirectiveUtilTypes.generated.h"
|
#include "DirectiveUtilTypes.generated.h"
|
||||||
@@ -12,3 +14,39 @@ enum class EDirectiveUtilSuccessStatus : uint8
|
|||||||
Success,
|
Success,
|
||||||
Failure,
|
Failure,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EDirectiveUtilWorldType : uint8
|
||||||
|
{
|
||||||
|
Unknown,
|
||||||
|
None,
|
||||||
|
Game,
|
||||||
|
Editor,
|
||||||
|
PlayInEditor UMETA(DisplayName = "Play In Editor"),
|
||||||
|
EditorPreview UMETA(DisplayName = "Editor Preview"),
|
||||||
|
GamePreview UMETA(DisplayName = "Game Preview"),
|
||||||
|
GameRPC UMETA(DisplayName = "Game RPC"),
|
||||||
|
Inactive,
|
||||||
|
};
|
||||||
|
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EDirectiveUtilBuildConfiguration : uint8
|
||||||
|
{
|
||||||
|
Unknown,
|
||||||
|
Debug,
|
||||||
|
DebugGame UMETA(DisplayName = "Debug Game"),
|
||||||
|
Development,
|
||||||
|
Shipping,
|
||||||
|
Test,
|
||||||
|
};
|
||||||
|
|
||||||
|
UENUM(BlueprintType)
|
||||||
|
enum class EDirectiveUtilBuildTargetType : uint8
|
||||||
|
{
|
||||||
|
Unknown,
|
||||||
|
Game,
|
||||||
|
Server,
|
||||||
|
Client,
|
||||||
|
Editor,
|
||||||
|
Program,
|
||||||
|
};
|
||||||
|
|||||||
@@ -23,13 +23,19 @@ public class DirectiveUtilitiesTests : ModuleRules
|
|||||||
"DirectiveUtilitiesRuntime",
|
"DirectiveUtilitiesRuntime",
|
||||||
"AutomationTest",
|
"AutomationTest",
|
||||||
"EnhancedInput",
|
"EnhancedInput",
|
||||||
"GameplayTags"
|
"GameplayTags",
|
||||||
|
"Projects"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (Target.bBuildEditor)
|
if (Target.bBuildEditor)
|
||||||
{
|
{
|
||||||
|
PrivateIncludePaths.Add(System.IO.Path.Combine(ModuleDirectory, "../DirectiveUtilitiesEditor/Private"));
|
||||||
|
PrivateDependencyModuleNames.Add("AssetRegistry");
|
||||||
|
PrivateDependencyModuleNames.Add("BlueprintGraph");
|
||||||
|
PrivateDependencyModuleNames.Add("DirectiveUtilitiesBlueprintNodes");
|
||||||
PrivateDependencyModuleNames.Add("DirectiveUtilitiesEditor");
|
PrivateDependencyModuleNames.Add("DirectiveUtilitiesEditor");
|
||||||
|
PrivateDependencyModuleNames.Add("UnrealEd");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "Libraries/DirectiveUtilArrayFunctionLibrary.h"
|
||||||
|
#include "Tests/DirectiveUtilTestObject.h"
|
||||||
|
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "EdGraphSchema_K2.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "K2Node_CallArrayFunction.h"
|
||||||
|
#include "K2Node_Event.h"
|
||||||
|
#include "K2Node_VariableGet.h"
|
||||||
|
#include "K2Node_VariableSet.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "Misc/AutomationTest.h"
|
||||||
|
#include "UObject/Package.h"
|
||||||
|
|
||||||
|
namespace DirectiveUtilArrayBlueprintVmTest
|
||||||
|
{
|
||||||
|
template <typename NodeType>
|
||||||
|
NodeType* AddNode(UEdGraph& Graph)
|
||||||
|
{
|
||||||
|
NodeType* Node = NewObject<NodeType>(&Graph);
|
||||||
|
Graph.AddNode(Node);
|
||||||
|
Node->CreateNewGuid();
|
||||||
|
Node->PostPlacedNewNode();
|
||||||
|
return Node;
|
||||||
|
}
|
||||||
|
|
||||||
|
UK2Node_VariableGet* AddVariableGet(UEdGraph& Graph, const FName PropertyName)
|
||||||
|
{
|
||||||
|
UK2Node_VariableGet* Node = AddNode<UK2Node_VariableGet>(Graph);
|
||||||
|
Node->VariableReference.SetSelfMember(PropertyName);
|
||||||
|
Node->AllocateDefaultPins();
|
||||||
|
return Node;
|
||||||
|
}
|
||||||
|
|
||||||
|
UK2Node_VariableSet* AddVariableSet(UEdGraph& Graph, const FName PropertyName)
|
||||||
|
{
|
||||||
|
UK2Node_VariableSet* Node = AddNode<UK2Node_VariableSet>(Graph);
|
||||||
|
Node->VariableReference.SetSelfMember(PropertyName);
|
||||||
|
Node->AllocateDefaultPins();
|
||||||
|
return Node;
|
||||||
|
}
|
||||||
|
|
||||||
|
UK2Node_CallArrayFunction* AddArrayCall(UEdGraph& Graph, const FName FunctionName)
|
||||||
|
{
|
||||||
|
UK2Node_CallArrayFunction* Node = AddNode<UK2Node_CallArrayFunction>(Graph);
|
||||||
|
Node->FunctionReference.SetExternalMember(FunctionName, UDirectiveUtilArrayFunctionLibrary::StaticClass());
|
||||||
|
Node->AllocateDefaultPins();
|
||||||
|
return Node;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Connect(UEdGraph& Graph, UEdGraphNode& FromNode, const FName FromPinName,
|
||||||
|
UEdGraphNode& ToNode, const FName ToPinName)
|
||||||
|
{
|
||||||
|
UEdGraphPin* FromPin = FromNode.FindPin(FromPinName);
|
||||||
|
UEdGraphPin* ToPin = ToNode.FindPin(ToPinName);
|
||||||
|
const UEdGraphSchema_K2* Schema = CastChecked<UEdGraphSchema_K2>(Graph.GetSchema());
|
||||||
|
return FromPin && ToPin && Schema->TryCreateConnection(FromPin, ToPin);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ConnectVariable(UEdGraph& Graph, const FName PropertyName,
|
||||||
|
UEdGraphNode& ToNode, const FName ToPinName)
|
||||||
|
{
|
||||||
|
UK2Node_VariableGet* VariableGet = AddVariableGet(Graph, PropertyName);
|
||||||
|
return Connect(Graph, *VariableGet, PropertyName, ToNode, ToPinName);
|
||||||
|
}
|
||||||
|
|
||||||
|
UBlueprint* BuildScenarioBlueprint(FAutomationTestBase& Test, const FName TargetProperty,
|
||||||
|
const FName SourceProperty, const FName ItemProperty)
|
||||||
|
{
|
||||||
|
const FName BlueprintName(*FString::Printf(
|
||||||
|
TEXT("BP_ArrayThunkVm_%s_%s"),
|
||||||
|
*TargetProperty.ToString(),
|
||||||
|
*FGuid::NewGuid().ToString(EGuidFormats::Digits)));
|
||||||
|
UPackage* Package = CreatePackage(
|
||||||
|
*FString::Printf(TEXT("/Temp/DirectiveUtilitiesTests/%s"), *BlueprintName.ToString()));
|
||||||
|
Package->SetFlags(RF_Transient);
|
||||||
|
UBlueprint* Blueprint = FKismetEditorUtilities::CreateBlueprint(
|
||||||
|
UDirectiveUtilTestObject::StaticClass(),
|
||||||
|
Package,
|
||||||
|
BlueprintName,
|
||||||
|
BPTYPE_Normal,
|
||||||
|
TEXT("DirectiveUtilities.ArrayBlueprintVmTests"));
|
||||||
|
if (!Test.TestNotNull(TEXT("The array VM test Blueprint should be created"), Blueprint))
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraph* Graph = FBlueprintEditorUtils::FindEventGraph(Blueprint);
|
||||||
|
if (!Test.TestNotNull(TEXT("The array VM test Blueprint should have an event graph"), Graph))
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
UK2Node_Event* Event = AddNode<UK2Node_Event>(*Graph);
|
||||||
|
Event->EventReference.SetExternalMember(
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilTestObject, RunArrayThunkScenario),
|
||||||
|
UDirectiveUtilTestObject::StaticClass());
|
||||||
|
Event->bOverrideFunction = true;
|
||||||
|
Event->AllocateDefaultPins();
|
||||||
|
|
||||||
|
UK2Node_CallArrayFunction* Append = AddArrayCall(
|
||||||
|
*Graph,
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_AppendOptimized));
|
||||||
|
UK2Node_CallArrayFunction* Insert = AddArrayCall(
|
||||||
|
*Graph,
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_InsertOptimized));
|
||||||
|
UK2Node_VariableSet* SetInsertResult = AddVariableSet(*Graph, TEXT("TestInsertResult"));
|
||||||
|
UK2Node_CallArrayFunction* RemoveIndices = AddArrayCall(
|
||||||
|
*Graph,
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_RemoveAtIndices));
|
||||||
|
UK2Node_VariableSet* SetRemovedCount = AddVariableSet(*Graph, TEXT("TestRemovedCount"));
|
||||||
|
UK2Node_CallArrayFunction* RemoveAll = AddArrayCall(
|
||||||
|
*Graph,
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_RemoveAllOccurrences));
|
||||||
|
UK2Node_VariableSet* SetRemoveAllResult = AddVariableSet(*Graph, TEXT("TestRemoveAllResult"));
|
||||||
|
|
||||||
|
bool bConnected = true;
|
||||||
|
bConnected &= Connect(*Graph, *Event, UEdGraphSchema_K2::PN_Then, *Append, UEdGraphSchema_K2::PN_Execute);
|
||||||
|
bConnected &= Connect(*Graph, *Append, UEdGraphSchema_K2::PN_Then, *Insert, UEdGraphSchema_K2::PN_Execute);
|
||||||
|
bConnected &= Connect(*Graph, *Insert, UEdGraphSchema_K2::PN_Then, *SetInsertResult, UEdGraphSchema_K2::PN_Execute);
|
||||||
|
bConnected &= Connect(*Graph, *Insert, UEdGraphSchema_K2::PN_ReturnValue, *SetInsertResult, TEXT("TestInsertResult"));
|
||||||
|
bConnected &= Connect(*Graph, *SetInsertResult, UEdGraphSchema_K2::PN_Then, *RemoveIndices, UEdGraphSchema_K2::PN_Execute);
|
||||||
|
bConnected &= Connect(*Graph, *RemoveIndices, UEdGraphSchema_K2::PN_Then, *SetRemovedCount, UEdGraphSchema_K2::PN_Execute);
|
||||||
|
bConnected &= Connect(*Graph, *RemoveIndices, UEdGraphSchema_K2::PN_ReturnValue, *SetRemovedCount, TEXT("TestRemovedCount"));
|
||||||
|
bConnected &= Connect(*Graph, *SetRemovedCount, UEdGraphSchema_K2::PN_Then, *RemoveAll, UEdGraphSchema_K2::PN_Execute);
|
||||||
|
bConnected &= Connect(*Graph, *RemoveAll, UEdGraphSchema_K2::PN_Then, *SetRemoveAllResult, UEdGraphSchema_K2::PN_Execute);
|
||||||
|
bConnected &= Connect(*Graph, *RemoveAll, UEdGraphSchema_K2::PN_ReturnValue, *SetRemoveAllResult, TEXT("TestRemoveAllResult"));
|
||||||
|
|
||||||
|
for (UK2Node_CallArrayFunction* Call : {Append, Insert, RemoveIndices, RemoveAll})
|
||||||
|
{
|
||||||
|
bConnected &= ConnectVariable(*Graph, TargetProperty, *Call, TEXT("TargetArray"));
|
||||||
|
}
|
||||||
|
bConnected &= ConnectVariable(*Graph, SourceProperty, *Append, TEXT("SourceArray"));
|
||||||
|
bConnected &= ConnectVariable(*Graph, SourceProperty, *Insert, TEXT("SourceArray"));
|
||||||
|
bConnected &= ConnectVariable(*Graph, TEXT("TestIndices"), *RemoveIndices, TEXT("Indices"));
|
||||||
|
bConnected &= ConnectVariable(*Graph, ItemProperty, *RemoveAll, TEXT("Item"));
|
||||||
|
Insert->FindPinChecked(TEXT("Index"))->DefaultValue = TEXT("1");
|
||||||
|
|
||||||
|
if (!Test.TestTrue(TEXT("The array VM test graph should connect every pin"), bConnected))
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint);
|
||||||
|
FKismetEditorUtilities::CompileBlueprint(Blueprint, EBlueprintCompileOptions::SkipGarbageCollection);
|
||||||
|
if (!Test.TestTrue(
|
||||||
|
TEXT("The array VM test Blueprint should compile"),
|
||||||
|
Blueprint->Status == BS_UpToDate || Blueprint->Status == BS_UpToDateWithWarnings))
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return Blueprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
UDirectiveUtilTestObject* CreateScenarioInstance(FAutomationTestBase& Test, UBlueprint& Blueprint)
|
||||||
|
{
|
||||||
|
UDirectiveUtilTestObject* Instance = NewObject<UDirectiveUtilTestObject>(
|
||||||
|
GetTransientPackage(), Blueprint.GeneratedClass);
|
||||||
|
if (!Test.TestNotNull(TEXT("The compiled array VM test instance should be created"), Instance))
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
Instance->TestIndices = {0, 4};
|
||||||
|
return Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RunScenario(
|
||||||
|
FAutomationTestBase& Test,
|
||||||
|
UDirectiveUtilTestObject& Instance,
|
||||||
|
const bool bExpectedInsertResult = true,
|
||||||
|
const int32 ExpectedRemovedCount = 2,
|
||||||
|
const bool bExpectedRemoveAllResult = true)
|
||||||
|
{
|
||||||
|
UFunction* Function = Instance.FindFunction(
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilTestObject, RunArrayThunkScenario));
|
||||||
|
if (!Test.TestNotNull(TEXT("The compiled array VM event should exist"), Function))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Instance.ProcessEvent(Function, nullptr);
|
||||||
|
Test.TestEqual(
|
||||||
|
TEXT("Insert Array Optimized should marshal its Blueprint result"),
|
||||||
|
Instance.TestInsertResult,
|
||||||
|
bExpectedInsertResult);
|
||||||
|
Test.TestEqual(
|
||||||
|
TEXT("Remove At Indices should marshal its Blueprint result"),
|
||||||
|
Instance.TestRemovedCount,
|
||||||
|
ExpectedRemovedCount);
|
||||||
|
Test.TestEqual(
|
||||||
|
TEXT("Remove All Occurrences should marshal its Blueprint result"),
|
||||||
|
Instance.TestRemoveAllResult,
|
||||||
|
bExpectedRemoveAllResult);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
|
FDirectiveUtilArrayBlueprintVmTest,
|
||||||
|
"DirectiveUtilities.ArrayBlueprintVmTests",
|
||||||
|
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FDirectiveUtilArrayBlueprintVmTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
using namespace DirectiveUtilArrayBlueprintVmTest;
|
||||||
|
|
||||||
|
if (UBlueprint* Blueprint = BuildScenarioBlueprint(
|
||||||
|
*this, TEXT("TestBoolArray"), TEXT("TestBoolSourceArray"), TEXT("TestBoolItem")))
|
||||||
|
{
|
||||||
|
if (UDirectiveUtilTestObject* Instance = CreateScenarioInstance(*this, *Blueprint))
|
||||||
|
{
|
||||||
|
Instance->TestBoolArray = {true, false, true};
|
||||||
|
Instance->TestBoolSourceArray = {false, true};
|
||||||
|
Instance->TestBoolItem = false;
|
||||||
|
if (RunScenario(*this, *Instance))
|
||||||
|
{
|
||||||
|
TestEqual(TEXT("Boolean wildcard values should survive Blueprint VM execution"),
|
||||||
|
Instance->TestBoolArray, TArray<bool>({true, true}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UBlueprint* Blueprint = BuildScenarioBlueprint(
|
||||||
|
*this, TEXT("TestBoolArray"), TEXT("TestBoolArray"), TEXT("TestBoolItem")))
|
||||||
|
{
|
||||||
|
if (UDirectiveUtilTestObject* Instance = CreateScenarioInstance(*this, *Blueprint))
|
||||||
|
{
|
||||||
|
Instance->TestBoolArray = {true, false};
|
||||||
|
Instance->TestBoolItem = true;
|
||||||
|
Instance->TestIndices = {-1, 99};
|
||||||
|
if (RunScenario(*this, *Instance, true, 0, true))
|
||||||
|
{
|
||||||
|
TestEqual(
|
||||||
|
TEXT("Self-aliasing wildcard arrays should survive Blueprint VM execution"),
|
||||||
|
Instance->TestBoolArray,
|
||||||
|
TArray<bool>({false, false, false, false}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UBlueprint* Blueprint = BuildScenarioBlueprint(
|
||||||
|
*this, TEXT("TestBoolArray"), TEXT("TestBoolSourceArray"), TEXT("TestBoolItem")))
|
||||||
|
{
|
||||||
|
if (UDirectiveUtilTestObject* Instance = CreateScenarioInstance(*this, *Blueprint))
|
||||||
|
{
|
||||||
|
Instance->TestBoolArray = {true};
|
||||||
|
Instance->TestBoolSourceArray.Reset();
|
||||||
|
Instance->TestBoolItem = false;
|
||||||
|
Instance->TestIndices = {-1, 99};
|
||||||
|
if (RunScenario(*this, *Instance, false, 0, false))
|
||||||
|
{
|
||||||
|
TestEqual(
|
||||||
|
TEXT("No-op wildcard calls should preserve the target through Blueprint VM execution"),
|
||||||
|
Instance->TestBoolArray,
|
||||||
|
TArray<bool>({true}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UBlueprint* Blueprint = BuildScenarioBlueprint(
|
||||||
|
*this, TEXT("TestStringArray"), TEXT("TestStringSourceArray"), TEXT("TestStringItem")))
|
||||||
|
{
|
||||||
|
if (UDirectiveUtilTestObject* Instance = CreateScenarioInstance(*this, *Blueprint))
|
||||||
|
{
|
||||||
|
Instance->TestStringArray = {TEXT("A"), TEXT("B"), TEXT("A")};
|
||||||
|
Instance->TestStringSourceArray = {TEXT("C"), TEXT("A")};
|
||||||
|
Instance->TestStringItem = TEXT("A");
|
||||||
|
if (RunScenario(*this, *Instance))
|
||||||
|
{
|
||||||
|
TestEqual(TEXT("String wildcard values should survive Blueprint VM execution"),
|
||||||
|
Instance->TestStringArray, TArray<FString>({TEXT("C"), TEXT("B"), TEXT("C")}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UBlueprint* Blueprint = BuildScenarioBlueprint(
|
||||||
|
*this, TEXT("TestCollisionArray"), TEXT("TestCollisionSourceArray"), TEXT("TestCollisionItem")))
|
||||||
|
{
|
||||||
|
if (UDirectiveUtilTestObject* Instance = CreateScenarioInstance(*this, *Blueprint))
|
||||||
|
{
|
||||||
|
Instance->TestCollisionArray = {{1}, {2}, {1}};
|
||||||
|
Instance->TestCollisionSourceArray = {{3}, {1}};
|
||||||
|
Instance->TestCollisionItem = {1};
|
||||||
|
if (RunScenario(*this, *Instance))
|
||||||
|
{
|
||||||
|
TestEqual(TEXT("Struct wildcard values should survive Blueprint VM execution"),
|
||||||
|
Instance->TestCollisionArray, TArray<FDirectiveUtilCollisionValue>({{3}, {2}, {3}}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UBlueprint* Blueprint = BuildScenarioBlueprint(
|
||||||
|
*this, TEXT("TestObjectArray"), TEXT("TestObjectSourceArray"), TEXT("TestObjectItem")))
|
||||||
|
{
|
||||||
|
if (UDirectiveUtilTestObject* Instance = CreateScenarioInstance(*this, *Blueprint))
|
||||||
|
{
|
||||||
|
UObject* A = NewObject<UDirectiveUtilTestObject>(Instance);
|
||||||
|
UObject* B = NewObject<UDirectiveUtilTestObject>(Instance);
|
||||||
|
UObject* C = NewObject<UDirectiveUtilTestObject>(Instance);
|
||||||
|
Instance->TestObjectArray = {A, B, A};
|
||||||
|
Instance->TestObjectSourceArray = {C, A};
|
||||||
|
Instance->TestObjectItem = A;
|
||||||
|
if (RunScenario(*this, *Instance))
|
||||||
|
{
|
||||||
|
TestEqual(TEXT("Object wildcard arrays should retain three values"), Instance->TestObjectArray.Num(), 3);
|
||||||
|
if (Instance->TestObjectArray.Num() == 3)
|
||||||
|
{
|
||||||
|
TestEqual(TEXT("Object wildcard arrays should retain the first source object"), Instance->TestObjectArray[0].Get(), C);
|
||||||
|
TestEqual(TEXT("Object wildcard arrays should retain the target object"), Instance->TestObjectArray[1].Get(), B);
|
||||||
|
TestEqual(TEXT("Object wildcard arrays should retain the second source object"), Instance->TestObjectArray[2].Get(), C);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,207 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "Misc/AutomationTest.h"
|
||||||
|
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "EdGraphSchema_K2.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "K2Node_CallArrayFunction.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "Kismet2/CompilerResultsLog.h"
|
||||||
|
#include "Libraries/DirectiveUtilArrayFunctionLibrary.h"
|
||||||
|
#include "Tests/DirectiveUtilTestObject.h"
|
||||||
|
|
||||||
|
namespace DirectiveUtilArrayNodeTest
|
||||||
|
{
|
||||||
|
struct FDependentPin
|
||||||
|
{
|
||||||
|
FName Name;
|
||||||
|
EPinContainerType ContainerType;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FArrayNodeCase
|
||||||
|
{
|
||||||
|
FName FunctionName;
|
||||||
|
TArray<FDependentPin> DependentPins;
|
||||||
|
};
|
||||||
|
|
||||||
|
FEdGraphPinType MakeArrayType(
|
||||||
|
const FName Category,
|
||||||
|
const FName SubCategory = NAME_None,
|
||||||
|
UObject* SubCategoryObject = nullptr)
|
||||||
|
{
|
||||||
|
FEdGraphPinType PinType;
|
||||||
|
PinType.PinCategory = Category;
|
||||||
|
PinType.PinSubCategory = SubCategory;
|
||||||
|
PinType.PinSubCategoryObject = SubCategoryObject;
|
||||||
|
PinType.ContainerType = EPinContainerType::Array;
|
||||||
|
return PinType;
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraph* MakeGraph()
|
||||||
|
{
|
||||||
|
UBlueprint* Blueprint = NewObject<UBlueprint>();
|
||||||
|
Blueprint->GeneratedClass = UObject::StaticClass();
|
||||||
|
Blueprint->SkeletonGeneratedClass = UObject::StaticClass();
|
||||||
|
UEdGraph* Graph = NewObject<UEdGraph>(Blueprint);
|
||||||
|
Graph->Schema = UEdGraphSchema_K2::StaticClass();
|
||||||
|
Blueprint->UbergraphPages.Add(Graph);
|
||||||
|
return Graph;
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraphPin* AddArrayOutput(UEdGraph& Graph, const FName Name, const FEdGraphPinType& PinType)
|
||||||
|
{
|
||||||
|
UEdGraphNode* SourceNode = NewObject<UEdGraphNode>(&Graph);
|
||||||
|
Graph.AddNode(SourceNode);
|
||||||
|
return SourceNode->CreatePin(EGPD_Output, PinType, Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
UK2Node_CallArrayFunction* AddFunctionNode(UEdGraph& Graph, const FName FunctionName)
|
||||||
|
{
|
||||||
|
UK2Node_CallArrayFunction* Node = NewObject<UK2Node_CallArrayFunction>(&Graph);
|
||||||
|
Graph.AddNode(Node);
|
||||||
|
const UFunction* Function = UDirectiveUtilArrayFunctionLibrary::StaticClass()->FindFunctionByName(FunctionName);
|
||||||
|
Node->FunctionReference.SetExternalMember(FunctionName, UDirectiveUtilArrayFunctionLibrary::StaticClass());
|
||||||
|
Node->bDefaultsToPureFunc = Function && Function->HasAnyFunctionFlags(FUNC_BlueprintPure);
|
||||||
|
Node->AllocateDefaultPins();
|
||||||
|
return Node;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Connect(UEdGraphPin& OutputPin, UK2Node& Node, UEdGraphPin& InputPin)
|
||||||
|
{
|
||||||
|
OutputPin.MakeLinkTo(&InputPin);
|
||||||
|
Node.PinConnectionListChanged(&InputPin);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Disconnect(UEdGraphPin& OutputPin, UK2Node& Node, UEdGraphPin& InputPin)
|
||||||
|
{
|
||||||
|
OutputPin.BreakLinkTo(&InputPin);
|
||||||
|
Node.PinConnectionListChanged(&InputPin);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasElementType(
|
||||||
|
const UEdGraphPin& Pin,
|
||||||
|
const FEdGraphPinType& ArrayType,
|
||||||
|
const EPinContainerType ContainerType)
|
||||||
|
{
|
||||||
|
return Pin.PinType.ContainerType == ContainerType
|
||||||
|
&& Pin.PinType.PinCategory == ArrayType.PinCategory
|
||||||
|
&& Pin.PinType.PinSubCategory == ArrayType.PinSubCategory
|
||||||
|
&& Pin.PinType.PinSubCategoryObject == ArrayType.PinSubCategoryObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
|
FDirectiveUtilArrayNodeWildcardTest,
|
||||||
|
"DirectiveUtilities.ArrayNodeWildcardTests",
|
||||||
|
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FDirectiveUtilArrayNodeWildcardTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
using namespace DirectiveUtilArrayNodeTest;
|
||||||
|
|
||||||
|
const TArray<FArrayNodeCase> NodeCases = {
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_NextIndex), {}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_PreviousIndex), {}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_RemoveDuplicates), {}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_AppendOptimized), {{TEXT("SourceArray"), EPinContainerType::Array}}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_InsertOptimized), {{TEXT("SourceArray"), EPinContainerType::Array}}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_GetValidFirstItemCopy), {{TEXT("OutItem"), EPinContainerType::None}}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_GetValidLastItemCopy), {{TEXT("OutItem"), EPinContainerType::None}}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_GetValidItemFromIndexCopy), {{TEXT("OutItem"), EPinContainerType::None}}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_GetRandomItem), {{TEXT("OutItem"), EPinContainerType::None}}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_LastValue), {{TEXT("OutItem"), EPinContainerType::None}}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_Pop), {{TEXT("OutItem"), EPinContainerType::None}}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_PopFirst), {{TEXT("OutItem"), EPinContainerType::None}}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_RemoveAtSwap), {}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_RemoveAtIndices), {}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_RemoveAllOccurrences), {{TEXT("Item"), EPinContainerType::None}}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_Slice), {{TEXT("OutArray"), EPinContainerType::Array}}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_Rotate), {}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_GetDistinct), {{TEXT("OutArray"), EPinContainerType::Array}}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_CountOccurrences), {{TEXT("ItemToCount"), EPinContainerType::None}}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_GetMostCommon), {{TEXT("OutItem"), EPinContainerType::None}}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_Sample), {{TEXT("OutArray"), EPinContainerType::Array}}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_SampleFromStream), {{TEXT("OutArray"), EPinContainerType::Array}}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_SampleWeighted), {{TEXT("OutArray"), EPinContainerType::Array}}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_SampleWeightedFromStream), {{TEXT("OutArray"), EPinContainerType::Array}}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_GetPage), {{TEXT("OutArray"), EPinContainerType::Array}}}
|
||||||
|
};
|
||||||
|
|
||||||
|
const TArray<FEdGraphPinType> ArrayTypes = {
|
||||||
|
MakeArrayType(UEdGraphSchema_K2::PC_Boolean),
|
||||||
|
MakeArrayType(UEdGraphSchema_K2::PC_String),
|
||||||
|
MakeArrayType(UEdGraphSchema_K2::PC_Object, NAME_None, UDirectiveUtilTestObject::StaticClass()),
|
||||||
|
MakeArrayType(UEdGraphSchema_K2::PC_Struct, NAME_None, FDirectiveUtilCollisionValue::StaticStruct())
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const FArrayNodeCase& NodeCase : NodeCases)
|
||||||
|
{
|
||||||
|
UEdGraph* Graph = MakeGraph();
|
||||||
|
UK2Node_CallArrayFunction* Node = AddFunctionNode(*Graph, NodeCase.FunctionName);
|
||||||
|
UEdGraphPin* TargetArray = Node->FindPin(TEXT("TargetArray"));
|
||||||
|
TestNotNull(*FString::Printf(TEXT("%s should have a TargetArray pin"), *NodeCase.FunctionName.ToString()), TargetArray);
|
||||||
|
if (!TargetArray)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestTrue(
|
||||||
|
*FString::Printf(TEXT("%s should begin as a wildcard array"), *NodeCase.FunctionName.ToString()),
|
||||||
|
TargetArray->PinType.IsArray() && TargetArray->PinType.PinCategory == UEdGraphSchema_K2::PC_Wildcard);
|
||||||
|
if (NodeCase.FunctionName == GET_FUNCTION_NAME_CHECKED(UDirectiveUtilArrayFunctionLibrary, Array_RemoveAtIndices))
|
||||||
|
{
|
||||||
|
const UEdGraphPin* IndicesPin = Node->FindPin(TEXT("Indices"));
|
||||||
|
TestNotNull(TEXT("RemoveAtIndices should have an Indices pin"), IndicesPin);
|
||||||
|
if (IndicesPin)
|
||||||
|
{
|
||||||
|
TestTrue(
|
||||||
|
TEXT("RemoveAtIndices should keep Indices as an integer array"),
|
||||||
|
IndicesPin->PinType.IsArray()
|
||||||
|
&& IndicesPin->PinType.PinCategory == UEdGraphSchema_K2::PC_Int);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FCompilerResultsLog ModuleValidationLog;
|
||||||
|
FBlueprintEditorUtils::ValidateEditorOnlyNodes(Node, ModuleValidationLog);
|
||||||
|
TestEqual(
|
||||||
|
*FString::Printf(TEXT("%s should be valid in a runtime Blueprint"), *NodeCase.FunctionName.ToString()),
|
||||||
|
ModuleValidationLog.NumWarnings,
|
||||||
|
0);
|
||||||
|
|
||||||
|
for (int32 TypeIndex = 0; TypeIndex < ArrayTypes.Num(); ++TypeIndex)
|
||||||
|
{
|
||||||
|
const FEdGraphPinType& ArrayType = ArrayTypes[TypeIndex];
|
||||||
|
UEdGraphPin* ArrayOutput = AddArrayOutput(
|
||||||
|
*Graph,
|
||||||
|
*FString::Printf(TEXT("ArrayOutput%d"), TypeIndex),
|
||||||
|
ArrayType);
|
||||||
|
Connect(*ArrayOutput, *Node, *TargetArray);
|
||||||
|
TestTrue(
|
||||||
|
*FString::Printf(TEXT("%s should resolve TargetArray type %d"), *NodeCase.FunctionName.ToString(), TypeIndex),
|
||||||
|
HasElementType(*TargetArray, ArrayType, EPinContainerType::Array));
|
||||||
|
|
||||||
|
for (const FDependentPin& ExpectedPin : NodeCase.DependentPins)
|
||||||
|
{
|
||||||
|
const UEdGraphPin* DependentPin = Node->FindPin(ExpectedPin.Name);
|
||||||
|
TestNotNull(
|
||||||
|
*FString::Printf(TEXT("%s should have a %s pin"), *NodeCase.FunctionName.ToString(), *ExpectedPin.Name.ToString()),
|
||||||
|
DependentPin);
|
||||||
|
if (DependentPin)
|
||||||
|
{
|
||||||
|
TestTrue(
|
||||||
|
*FString::Printf(TEXT("%s should resolve %s type %d"), *NodeCase.FunctionName.ToString(), *ExpectedPin.Name.ToString(), TypeIndex),
|
||||||
|
HasElementType(*DependentPin, ArrayType, ExpectedPin.ContainerType));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Disconnect(*ArrayOutput, *Node, *TargetArray);
|
||||||
|
TestTrue(
|
||||||
|
*FString::Printf(TEXT("%s should reset after type %d"), *NodeCase.FunctionName.ToString(), TypeIndex),
|
||||||
|
TargetArray->PinType.IsArray() && TargetArray->PinType.PinCategory == UEdGraphSchema_K2::PC_Wildcard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,801 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "Libraries/DirectiveUtilArrayFunctionLibrary.h"
|
||||||
|
#include "Tests/DirectiveUtilTestObject.h"
|
||||||
|
|
||||||
|
#include "Algo/Reverse.h"
|
||||||
|
#include "Misc/AutomationTest.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
FArrayProperty* GetIntegerArrayProperty()
|
||||||
|
{
|
||||||
|
return FindFProperty<FArrayProperty>(
|
||||||
|
UDirectiveUtilTestObject::StaticClass(),
|
||||||
|
GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestArray));
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<int32> MakeSequentialValues(const int32 Count)
|
||||||
|
{
|
||||||
|
TArray<int32> Values;
|
||||||
|
Values.SetNumUninitialized(Count);
|
||||||
|
for (int32 Index = 0; Index < Count; ++Index)
|
||||||
|
{
|
||||||
|
Values[Index] = Index;
|
||||||
|
}
|
||||||
|
return Values;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<int32> MakeRepeatingValues(const int32 Count, const int32 DistinctCount)
|
||||||
|
{
|
||||||
|
TArray<int32> Values;
|
||||||
|
Values.Reserve(Count);
|
||||||
|
for (int32 Index = 0; Index < Count; ++Index)
|
||||||
|
{
|
||||||
|
Values.Add(Index % DistinctCount);
|
||||||
|
}
|
||||||
|
return Values;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<int32> MakeDistinctReference(const TArray<int32>& Values)
|
||||||
|
{
|
||||||
|
TArray<int32> Result;
|
||||||
|
for (const int32 Value : Values)
|
||||||
|
{
|
||||||
|
Result.AddUnique(Value);
|
||||||
|
}
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FindMostCommonReference(const TArray<int32>& Values, int32& OutValue, int32& OutCount)
|
||||||
|
{
|
||||||
|
OutValue = 0;
|
||||||
|
OutCount = 0;
|
||||||
|
TMap<int32, int32> Counts;
|
||||||
|
for (const int32 Value : Values)
|
||||||
|
{
|
||||||
|
++Counts.FindOrAdd(Value);
|
||||||
|
}
|
||||||
|
for (const TPair<int32, int32>& Pair : Counts)
|
||||||
|
{
|
||||||
|
OutCount = FMath::Max(OutCount, Pair.Value);
|
||||||
|
}
|
||||||
|
for (const int32 Value : Values)
|
||||||
|
{
|
||||||
|
if (Counts.FindRef(Value) == OutCount)
|
||||||
|
{
|
||||||
|
OutValue = Value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !Values.IsEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 CountReference(const TArray<int32>& Values, const int32 QueryValue)
|
||||||
|
{
|
||||||
|
int32 Count = 0;
|
||||||
|
for (const int32 Value : Values)
|
||||||
|
{
|
||||||
|
Count += Value == QueryValue ? 1 : 0;
|
||||||
|
}
|
||||||
|
return Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<int32> RemoveAllReference(const TArray<int32>& Values, const int32 QueryValue)
|
||||||
|
{
|
||||||
|
TArray<int32> Result;
|
||||||
|
Result.Reserve(Values.Num());
|
||||||
|
for (const int32 Value : Values)
|
||||||
|
{
|
||||||
|
if (Value != QueryValue)
|
||||||
|
{
|
||||||
|
Result.Add(Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<int32> SliceReference(const TArray<int32>& Values, const int32 StartIndex, const int32 Count)
|
||||||
|
{
|
||||||
|
TArray<int32> Result;
|
||||||
|
Result.Reserve(Count);
|
||||||
|
for (int32 Index = 0; Index < Count; ++Index)
|
||||||
|
{
|
||||||
|
Result.Add(Values[StartIndex + Index]);
|
||||||
|
}
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<int32> MakeSampleReference(const int32 SourceCount, const int32 RequestedCount, const int32 Seed)
|
||||||
|
{
|
||||||
|
TArray<int32> AvailableIndices = MakeSequentialValues(SourceCount);
|
||||||
|
TArray<int32> Result;
|
||||||
|
const int32 SampleCount = FMath::Clamp(RequestedCount, 0, SourceCount);
|
||||||
|
Result.Reserve(SampleCount);
|
||||||
|
FRandomStream RandomStream(Seed);
|
||||||
|
for (int32 SampleIndex = 0; SampleIndex < SampleCount; ++SampleIndex)
|
||||||
|
{
|
||||||
|
const int32 SelectedIndex = RandomStream.RandRange(0, AvailableIndices.Num() - 1);
|
||||||
|
Result.Add(AvailableIndices[SelectedIndex]);
|
||||||
|
AvailableIndices.RemoveAtSwap(SelectedIndex, 1, EAllowShrinking::No);
|
||||||
|
}
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ValueType>
|
||||||
|
TArray<ValueType> MakeRotationReference(const TArray<ValueType>& Values, const int32 Shift)
|
||||||
|
{
|
||||||
|
if (Values.IsEmpty())
|
||||||
|
{
|
||||||
|
return Values;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 NormalizedShift = Shift % Values.Num();
|
||||||
|
if (NormalizedShift < 0)
|
||||||
|
{
|
||||||
|
NormalizedShift += Values.Num();
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<ValueType> Result;
|
||||||
|
Result.Reserve(Values.Num());
|
||||||
|
for (int32 Index = 0; Index < Values.Num(); ++Index)
|
||||||
|
{
|
||||||
|
Result.Add(Values[(Index - NormalizedShift + Values.Num()) % Values.Num()]);
|
||||||
|
}
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
|
FDirectiveUtilArrayCardinalityTest,
|
||||||
|
"DirectiveUtilities.ArrayScenarios.Cardinality",
|
||||||
|
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FDirectiveUtilArrayCardinalityTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
FArrayProperty* ArrayProperty = GetIntegerArrayProperty();
|
||||||
|
if (!TestNotNull("Integer array property should be available", ArrayProperty))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UDirectiveUtilTestObject* TestObject = NewObject<UDirectiveUtilTestObject>();
|
||||||
|
struct FScenario
|
||||||
|
{
|
||||||
|
int32 ItemCount;
|
||||||
|
int32 DistinctCount;
|
||||||
|
};
|
||||||
|
const TArray<FScenario> Scenarios = {
|
||||||
|
{0, 1},
|
||||||
|
{1, 1},
|
||||||
|
{2, 1},
|
||||||
|
{3, 2},
|
||||||
|
{16, 16},
|
||||||
|
{31, 7},
|
||||||
|
{64, 4},
|
||||||
|
{257, 257},
|
||||||
|
{1024, 1},
|
||||||
|
{4096, 257},
|
||||||
|
{16384, 1024}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const FScenario& Scenario : Scenarios)
|
||||||
|
{
|
||||||
|
const TArray<int32> Source = MakeRepeatingValues(Scenario.ItemCount, Scenario.DistinctCount);
|
||||||
|
const TArray<int32> ExpectedDistinct = MakeDistinctReference(Source);
|
||||||
|
const FString Label = FString::Printf(
|
||||||
|
TEXT("items=%d distinct=%d"),
|
||||||
|
Scenario.ItemCount,
|
||||||
|
FMath::Min(Scenario.ItemCount, Scenario.DistinctCount));
|
||||||
|
|
||||||
|
TestObject->TestArray = Source;
|
||||||
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveDuplicates(&TestObject->TestArray, ArrayProperty);
|
||||||
|
TestEqual(Label + TEXT(" RemoveDuplicates"), TestObject->TestArray, ExpectedDistinct);
|
||||||
|
|
||||||
|
TArray<int32> DistinctResult;
|
||||||
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_GetDistinct(
|
||||||
|
&Source,
|
||||||
|
ArrayProperty,
|
||||||
|
&DistinctResult,
|
||||||
|
ArrayProperty);
|
||||||
|
TestEqual(Label + TEXT(" GetDistinct"), DistinctResult, ExpectedDistinct);
|
||||||
|
|
||||||
|
int32 MostCommonValue = INDEX_NONE;
|
||||||
|
int32 MostCommonCount = INDEX_NONE;
|
||||||
|
const bool bFoundMostCommon = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetMostCommon(
|
||||||
|
&Source,
|
||||||
|
ArrayProperty,
|
||||||
|
&MostCommonValue,
|
||||||
|
&MostCommonCount);
|
||||||
|
if (Source.IsEmpty())
|
||||||
|
{
|
||||||
|
TestFalse(Label + TEXT(" GetMostCommon should fail"), bFoundMostCommon);
|
||||||
|
TestEqual(Label + TEXT(" GetMostCommon count"), MostCommonCount, 0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TestTrue(Label + TEXT(" GetMostCommon should succeed"), bFoundMostCommon);
|
||||||
|
TestEqual(Label + TEXT(" GetMostCommon value"), MostCommonValue, 0);
|
||||||
|
TestEqual(
|
||||||
|
Label + TEXT(" GetMostCommon count"),
|
||||||
|
MostCommonCount,
|
||||||
|
FMath::DivideAndRoundUp(Scenario.ItemCount, Scenario.DistinctCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
|
FDirectiveUtilArraySamplingScenarioTest,
|
||||||
|
"DirectiveUtilities.ArrayScenarios.Sampling",
|
||||||
|
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FDirectiveUtilArraySamplingScenarioTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
FArrayProperty* ArrayProperty = GetIntegerArrayProperty();
|
||||||
|
if (!TestNotNull("Integer array property should be available", ArrayProperty))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int32 Seed = 7351;
|
||||||
|
for (const int32 SourceCount : {0, 1, 2, 3, 4, 17, 100, 1000})
|
||||||
|
{
|
||||||
|
const TArray<int32> Source = MakeSequentialValues(SourceCount);
|
||||||
|
const TArray<int32> RequestedCounts = {-1, 0, 1, SourceCount / 4, SourceCount, SourceCount + 5};
|
||||||
|
for (const int32 RequestedCount : RequestedCounts)
|
||||||
|
{
|
||||||
|
FRandomStream FirstStream(Seed);
|
||||||
|
FRandomStream SecondStream(Seed);
|
||||||
|
TArray<int32> FirstSample;
|
||||||
|
TArray<int32> SecondSample;
|
||||||
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(
|
||||||
|
&Source,
|
||||||
|
ArrayProperty,
|
||||||
|
RequestedCount,
|
||||||
|
false,
|
||||||
|
&FirstStream,
|
||||||
|
&FirstSample,
|
||||||
|
ArrayProperty);
|
||||||
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(
|
||||||
|
&Source,
|
||||||
|
ArrayProperty,
|
||||||
|
RequestedCount,
|
||||||
|
false,
|
||||||
|
&SecondStream,
|
||||||
|
&SecondSample,
|
||||||
|
ArrayProperty);
|
||||||
|
|
||||||
|
const FString Label = FString::Printf(TEXT("source=%d requested=%d"), SourceCount, RequestedCount);
|
||||||
|
TestEqual(Label + TEXT(" count"), FirstSample.Num(), FMath::Clamp(RequestedCount, 0, SourceCount));
|
||||||
|
TestEqual(Label + TEXT(" deterministic"), FirstSample, SecondSample);
|
||||||
|
TSet<int32> UniqueValues;
|
||||||
|
for (const int32 Value : FirstSample)
|
||||||
|
{
|
||||||
|
TestTrue(Label + TEXT(" source membership"), Source.Contains(Value));
|
||||||
|
UniqueValues.Add(Value);
|
||||||
|
}
|
||||||
|
TestEqual(Label + TEXT(" uniqueness"), UniqueValues.Num(), FirstSample.Num());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TArray<int32> BoundarySource = MakeSequentialValues(100);
|
||||||
|
for (const int32 RequestedCount : {24, 25, 26, 75, 100})
|
||||||
|
{
|
||||||
|
FRandomStream RandomStream(Seed);
|
||||||
|
TArray<int32> Sample;
|
||||||
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(
|
||||||
|
&BoundarySource,
|
||||||
|
ArrayProperty,
|
||||||
|
RequestedCount,
|
||||||
|
false,
|
||||||
|
&RandomStream,
|
||||||
|
&Sample,
|
||||||
|
ArrayProperty);
|
||||||
|
TestEqual(
|
||||||
|
FString::Printf(TEXT("threshold requested=%d"), RequestedCount),
|
||||||
|
Sample,
|
||||||
|
MakeSampleReference(BoundarySource.Num(), RequestedCount, Seed));
|
||||||
|
}
|
||||||
|
|
||||||
|
const TArray<int32> ReplacementSource = MakeSequentialValues(7);
|
||||||
|
for (const int32 RequestedCount : {0, 1, 7, 14, 100})
|
||||||
|
{
|
||||||
|
FRandomStream FirstStream(Seed);
|
||||||
|
FRandomStream SecondStream(Seed);
|
||||||
|
TArray<int32> FirstSample;
|
||||||
|
TArray<int32> SecondSample;
|
||||||
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(
|
||||||
|
&ReplacementSource,
|
||||||
|
ArrayProperty,
|
||||||
|
RequestedCount,
|
||||||
|
true,
|
||||||
|
&FirstStream,
|
||||||
|
&FirstSample,
|
||||||
|
ArrayProperty);
|
||||||
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Sample(
|
||||||
|
&ReplacementSource,
|
||||||
|
ArrayProperty,
|
||||||
|
RequestedCount,
|
||||||
|
true,
|
||||||
|
&SecondStream,
|
||||||
|
&SecondSample,
|
||||||
|
ArrayProperty);
|
||||||
|
const FString Label = FString::Printf(TEXT("replacement requested=%d"), RequestedCount);
|
||||||
|
TestEqual(Label + TEXT(" count"), FirstSample.Num(), RequestedCount);
|
||||||
|
TestEqual(Label + TEXT(" deterministic"), FirstSample, SecondSample);
|
||||||
|
for (const int32 Value : FirstSample)
|
||||||
|
{
|
||||||
|
TestTrue(Label + TEXT(" source membership"), ReplacementSource.Contains(Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
|
FDirectiveUtilArrayWeightedSamplingScenarioTest,
|
||||||
|
"DirectiveUtilities.ArrayScenarios.WeightedSampling",
|
||||||
|
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FDirectiveUtilArrayWeightedSamplingScenarioTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
FArrayProperty* ArrayProperty = GetIntegerArrayProperty();
|
||||||
|
if (!TestNotNull("Integer array property should be available", ArrayProperty))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int32 Seed = 4815;
|
||||||
|
for (const int32 SourceCount : {0, 1, 2, 3, 17, 100, 1000, 4096})
|
||||||
|
{
|
||||||
|
const TArray<int32> Source = MakeSequentialValues(SourceCount);
|
||||||
|
TArray<float> Weights;
|
||||||
|
Weights.Reserve(SourceCount);
|
||||||
|
TSet<int32> SelectableValues;
|
||||||
|
for (int32 Index = 0; Index < SourceCount; ++Index)
|
||||||
|
{
|
||||||
|
const float Weight = Index % 3 == 0 ? 0.0f : static_cast<float>((Index % 7) + 1);
|
||||||
|
Weights.Add(Weight);
|
||||||
|
if (Weight > 0.0f)
|
||||||
|
{
|
||||||
|
SelectableValues.Add(Index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TArray<int32> RequestedCounts = {0, 1, SourceCount / 4, SourceCount, SourceCount + 5};
|
||||||
|
for (const int32 RequestedCount : RequestedCounts)
|
||||||
|
{
|
||||||
|
for (const bool bWithReplacement : {false, true})
|
||||||
|
{
|
||||||
|
FRandomStream FirstStream(Seed);
|
||||||
|
FRandomStream SecondStream(Seed);
|
||||||
|
TArray<int32> FirstSample;
|
||||||
|
TArray<int32> SecondSample;
|
||||||
|
const bool bFirstSucceeded = UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(
|
||||||
|
&Source,
|
||||||
|
ArrayProperty,
|
||||||
|
Weights,
|
||||||
|
RequestedCount,
|
||||||
|
bWithReplacement,
|
||||||
|
&FirstStream,
|
||||||
|
&FirstSample,
|
||||||
|
ArrayProperty);
|
||||||
|
const bool bSecondSucceeded = UDirectiveUtilArrayFunctionLibrary::GenericArray_SampleWeighted(
|
||||||
|
&Source,
|
||||||
|
ArrayProperty,
|
||||||
|
Weights,
|
||||||
|
RequestedCount,
|
||||||
|
bWithReplacement,
|
||||||
|
&SecondStream,
|
||||||
|
&SecondSample,
|
||||||
|
ArrayProperty);
|
||||||
|
const FString Label = FString::Printf(
|
||||||
|
TEXT("source=%d requested=%d replacement=%d"),
|
||||||
|
SourceCount,
|
||||||
|
RequestedCount,
|
||||||
|
bWithReplacement);
|
||||||
|
|
||||||
|
const bool bExpectedSuccess = RequestedCount == 0 || !SelectableValues.IsEmpty();
|
||||||
|
TestEqual(Label + TEXT(" first validity"), bFirstSucceeded, bExpectedSuccess);
|
||||||
|
TestEqual(Label + TEXT(" second validity"), bSecondSucceeded, bExpectedSuccess);
|
||||||
|
TestEqual(Label + TEXT(" deterministic"), FirstSample, SecondSample);
|
||||||
|
const int32 ExpectedCount = !bExpectedSuccess
|
||||||
|
? 0
|
||||||
|
: bWithReplacement
|
||||||
|
? RequestedCount
|
||||||
|
: FMath::Min(RequestedCount, SelectableValues.Num());
|
||||||
|
TestEqual(Label + TEXT(" count"), FirstSample.Num(), ExpectedCount);
|
||||||
|
|
||||||
|
TSet<int32> UniqueValues;
|
||||||
|
for (const int32 Value : FirstSample)
|
||||||
|
{
|
||||||
|
TestTrue(Label + TEXT(" selectable membership"), SelectableValues.Contains(Value));
|
||||||
|
UniqueValues.Add(Value);
|
||||||
|
}
|
||||||
|
if (!bWithReplacement)
|
||||||
|
{
|
||||||
|
TestEqual(Label + TEXT(" unique source indices"), UniqueValues.Num(), FirstSample.Num());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
|
FDirectiveUtilArrayRangeScenarioTest,
|
||||||
|
"DirectiveUtilities.ArrayScenarios.RangesAndPages",
|
||||||
|
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FDirectiveUtilArrayRangeScenarioTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
FArrayProperty* ArrayProperty = GetIntegerArrayProperty();
|
||||||
|
if (!TestNotNull("Integer array property should be available", ArrayProperty))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FPageScenario
|
||||||
|
{
|
||||||
|
int32 ItemCount;
|
||||||
|
int32 PageIndex;
|
||||||
|
int32 PageSize;
|
||||||
|
bool bExpectedValid;
|
||||||
|
int32 ExpectedPageCount;
|
||||||
|
};
|
||||||
|
const TArray<FPageScenario> PageScenarios = {
|
||||||
|
{0, 0, 1, false, 0},
|
||||||
|
{1, 0, 1, true, 1},
|
||||||
|
{2, 1, 1, true, 2},
|
||||||
|
{5, 0, 2, true, 3},
|
||||||
|
{5, 1, 2, true, 3},
|
||||||
|
{5, 2, 2, true, 3},
|
||||||
|
{5, 3, 2, false, 3},
|
||||||
|
{10, 3, 3, true, 4},
|
||||||
|
{10, 0, 10, true, 1},
|
||||||
|
{10, 0, 11, true, 1},
|
||||||
|
{10, -1, 3, false, 0},
|
||||||
|
{10, 0, 0, false, 0},
|
||||||
|
{10, 0, -1, false, 0},
|
||||||
|
{10, MAX_int32, 3, false, 4}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const FPageScenario& Scenario : PageScenarios)
|
||||||
|
{
|
||||||
|
const TArray<int32> Source = MakeSequentialValues(Scenario.ItemCount);
|
||||||
|
TArray<int32> Page;
|
||||||
|
int32 PageCount = INDEX_NONE;
|
||||||
|
const bool bValid = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetPage(
|
||||||
|
&Source,
|
||||||
|
ArrayProperty,
|
||||||
|
Scenario.PageIndex,
|
||||||
|
Scenario.PageSize,
|
||||||
|
&Page,
|
||||||
|
ArrayProperty,
|
||||||
|
&PageCount);
|
||||||
|
const FString Label = FString::Printf(
|
||||||
|
TEXT("items=%d page=%d size=%d"),
|
||||||
|
Scenario.ItemCount,
|
||||||
|
Scenario.PageIndex,
|
||||||
|
Scenario.PageSize);
|
||||||
|
TestEqual(Label + TEXT(" validity"), bValid, Scenario.bExpectedValid);
|
||||||
|
TestEqual(Label + TEXT(" page count"), PageCount, Scenario.ExpectedPageCount);
|
||||||
|
|
||||||
|
TArray<int32> ExpectedPage;
|
||||||
|
if (Scenario.bExpectedValid)
|
||||||
|
{
|
||||||
|
const int32 StartIndex = Scenario.PageIndex * Scenario.PageSize;
|
||||||
|
for (int32 Index = StartIndex; Index < FMath::Min(StartIndex + Scenario.PageSize, Source.Num()); ++Index)
|
||||||
|
{
|
||||||
|
ExpectedPage.Add(Source[Index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TestEqual(Label + TEXT(" values"), Page, ExpectedPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TArray<int32> SliceSource = MakeSequentialValues(10);
|
||||||
|
struct FSliceScenario
|
||||||
|
{
|
||||||
|
int32 StartIndex;
|
||||||
|
int32 Count;
|
||||||
|
};
|
||||||
|
const TArray<FSliceScenario> SliceScenarios = {
|
||||||
|
{-5, 3},
|
||||||
|
{0, -1},
|
||||||
|
{0, 0},
|
||||||
|
{0, 10},
|
||||||
|
{0, 20},
|
||||||
|
{5, 3},
|
||||||
|
{9, 5},
|
||||||
|
{10, 1},
|
||||||
|
{11, 1},
|
||||||
|
{MAX_int32, MAX_int32}
|
||||||
|
};
|
||||||
|
for (const FSliceScenario& Scenario : SliceScenarios)
|
||||||
|
{
|
||||||
|
const int32 StartIndex = FMath::Clamp(Scenario.StartIndex, 0, SliceSource.Num());
|
||||||
|
const int32 CopyCount = Scenario.Count > 0
|
||||||
|
? FMath::Min(Scenario.Count, SliceSource.Num() - StartIndex)
|
||||||
|
: 0;
|
||||||
|
TArray<int32> ExpectedSlice;
|
||||||
|
for (int32 Offset = 0; Offset < CopyCount; ++Offset)
|
||||||
|
{
|
||||||
|
ExpectedSlice.Add(SliceSource[StartIndex + Offset]);
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<int32> Slice;
|
||||||
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(
|
||||||
|
&SliceSource,
|
||||||
|
ArrayProperty,
|
||||||
|
Scenario.StartIndex,
|
||||||
|
Scenario.Count,
|
||||||
|
&Slice,
|
||||||
|
ArrayProperty);
|
||||||
|
const FString Label = FString::Printf(TEXT("start=%d count=%d"), Scenario.StartIndex, Scenario.Count);
|
||||||
|
TestEqual(Label + TEXT(" separate output"), Slice, ExpectedSlice);
|
||||||
|
|
||||||
|
TArray<int32> AliasedSlice = SliceSource;
|
||||||
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(
|
||||||
|
&AliasedSlice,
|
||||||
|
ArrayProperty,
|
||||||
|
Scenario.StartIndex,
|
||||||
|
Scenario.Count,
|
||||||
|
&AliasedSlice,
|
||||||
|
ArrayProperty);
|
||||||
|
TestEqual(Label + TEXT(" aliased output"), AliasedSlice, ExpectedSlice);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
|
FDirectiveUtilArrayRotationScenarioTest,
|
||||||
|
"DirectiveUtilities.ArrayScenarios.Rotation",
|
||||||
|
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FDirectiveUtilArrayRotationScenarioTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
FArrayProperty* IntegerArrayProperty = GetIntegerArrayProperty();
|
||||||
|
FArrayProperty* StringArrayProperty = FindFProperty<FArrayProperty>(
|
||||||
|
UDirectiveUtilTestObject::StaticClass(),
|
||||||
|
GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestStringArray));
|
||||||
|
if (!TestNotNull("Integer array property should be available", IntegerArrayProperty)
|
||||||
|
|| !TestNotNull("String array property should be available", StringArrayProperty))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const int32 ItemCount : {0, 1, 2, 3, 4, 5, 16, 257, 10000})
|
||||||
|
{
|
||||||
|
const TArray<int32> Source = MakeSequentialValues(ItemCount);
|
||||||
|
const TArray<int32> Shifts = {
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
-1,
|
||||||
|
ItemCount,
|
||||||
|
ItemCount + 1,
|
||||||
|
-ItemCount - 1,
|
||||||
|
MAX_int32,
|
||||||
|
MIN_int32
|
||||||
|
};
|
||||||
|
for (const int32 Shift : Shifts)
|
||||||
|
{
|
||||||
|
TArray<int32> Rotated = Source;
|
||||||
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Rotate(&Rotated, IntegerArrayProperty, Shift);
|
||||||
|
TestEqual(
|
||||||
|
FString::Printf(TEXT("POD items=%d shift=%d"), ItemCount, Shift),
|
||||||
|
Rotated,
|
||||||
|
MakeRotationReference(Source, Shift));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const int32 ItemCount : {0, 1, 2, 5, 32})
|
||||||
|
{
|
||||||
|
TArray<FString> Source;
|
||||||
|
Source.Reserve(ItemCount);
|
||||||
|
for (int32 Index = 0; Index < ItemCount; ++Index)
|
||||||
|
{
|
||||||
|
Source.Add(FString::Printf(TEXT("Value%d"), Index));
|
||||||
|
}
|
||||||
|
for (const int32 Shift : {0, 1, -1, 7, -11, MAX_int32, MIN_int32})
|
||||||
|
{
|
||||||
|
TArray<FString> Rotated = Source;
|
||||||
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Rotate(&Rotated, StringArrayProperty, Shift);
|
||||||
|
TestEqual(
|
||||||
|
FString::Printf(TEXT("managed items=%d shift=%d"), ItemCount, Shift),
|
||||||
|
Rotated,
|
||||||
|
MakeRotationReference(Source, Shift));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
|
FDirectiveUtilArrayOrderingScenarioTest,
|
||||||
|
"DirectiveUtilities.ArrayScenarios.NaturalOrdering",
|
||||||
|
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FDirectiveUtilArrayOrderingScenarioTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
for (const int32 ItemCount : {0, 1, 2, 10, 100, 1000})
|
||||||
|
{
|
||||||
|
TArray<FString> Strings;
|
||||||
|
TArray<FString> ExpectedStrings;
|
||||||
|
TArray<FName> Names;
|
||||||
|
TArray<FName> ExpectedNames;
|
||||||
|
Strings.Reserve(ItemCount);
|
||||||
|
ExpectedStrings.Reserve(ItemCount);
|
||||||
|
Names.Reserve(ItemCount);
|
||||||
|
ExpectedNames.Reserve(ItemCount);
|
||||||
|
for (int32 Index = 0; Index < ItemCount; ++Index)
|
||||||
|
{
|
||||||
|
ExpectedStrings.Add(FString::Printf(TEXT("Item%d"), Index));
|
||||||
|
ExpectedNames.Add(FName(*FString::Printf(TEXT("Actor%d"), Index)));
|
||||||
|
}
|
||||||
|
for (int32 Index = ItemCount - 1; Index >= 0; --Index)
|
||||||
|
{
|
||||||
|
Strings.Add(ExpectedStrings[Index]);
|
||||||
|
Names.Add(ExpectedNames[Index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
UDirectiveUtilArrayFunctionLibrary::NaturalSortStringArray(Strings);
|
||||||
|
UDirectiveUtilArrayFunctionLibrary::NaturalSortNameArray(Names);
|
||||||
|
const FString Label = FString::Printf(TEXT("items=%d"), ItemCount);
|
||||||
|
TestEqual(Label + TEXT(" string ascending"), Strings, ExpectedStrings);
|
||||||
|
TestEqual(Label + TEXT(" name ascending"), Names, ExpectedNames);
|
||||||
|
|
||||||
|
UDirectiveUtilArrayFunctionLibrary::NaturalSortStringArray(Strings, true);
|
||||||
|
UDirectiveUtilArrayFunctionLibrary::NaturalSortNameArray(Names, true);
|
||||||
|
Algo::Reverse(ExpectedStrings);
|
||||||
|
Algo::Reverse(ExpectedNames);
|
||||||
|
TestEqual(Label + TEXT(" string descending"), Strings, ExpectedStrings);
|
||||||
|
TestEqual(Label + TEXT(" name descending"), Names, ExpectedNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
|
FDirectiveUtilArrayDeterministicFuzzTest,
|
||||||
|
"DirectiveUtilities.ArrayScenarios.DeterministicFuzz",
|
||||||
|
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FDirectiveUtilArrayDeterministicFuzzTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
FArrayProperty* ArrayProperty = GetIntegerArrayProperty();
|
||||||
|
if (!TestNotNull("Integer array property should be available", ArrayProperty))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UDirectiveUtilTestObject* TestObject = NewObject<UDirectiveUtilTestObject>();
|
||||||
|
const TArray<int32> ItemCounts = {0, 1, 2, 3, 7, 16, 31, 32, 127, 128, 1024, 4096};
|
||||||
|
for (const int32 Seed : {17, 271, 4099, 65537, 104729})
|
||||||
|
{
|
||||||
|
for (int32 CountIndex = 0; CountIndex < ItemCounts.Num(); ++CountIndex)
|
||||||
|
{
|
||||||
|
const int32 ItemCount = ItemCounts[CountIndex];
|
||||||
|
FRandomStream Stream(Seed + ItemCount * 31);
|
||||||
|
TArray<int32> Source;
|
||||||
|
Source.Reserve(ItemCount);
|
||||||
|
for (int32 Index = 0; Index < ItemCount; ++Index)
|
||||||
|
{
|
||||||
|
Source.Add(Stream.RandRange(-64, 64));
|
||||||
|
}
|
||||||
|
const FString Label = FString::Printf(TEXT("seed=%d items=%d"), Seed, ItemCount);
|
||||||
|
|
||||||
|
const TArray<int32> ExpectedDistinct = MakeDistinctReference(Source);
|
||||||
|
TestObject->TestArray = Source;
|
||||||
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveDuplicates(&TestObject->TestArray, ArrayProperty);
|
||||||
|
TestEqual(Label + TEXT(" RemoveDuplicates"), TestObject->TestArray, ExpectedDistinct);
|
||||||
|
|
||||||
|
TArray<int32> DistinctResult;
|
||||||
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_GetDistinct(
|
||||||
|
&Source,
|
||||||
|
ArrayProperty,
|
||||||
|
&DistinctResult,
|
||||||
|
ArrayProperty);
|
||||||
|
TestEqual(Label + TEXT(" GetDistinct"), DistinctResult, ExpectedDistinct);
|
||||||
|
|
||||||
|
int32 ExpectedMostCommon = 0;
|
||||||
|
int32 ExpectedMostCommonCount = 0;
|
||||||
|
const bool bExpectedMostCommon = FindMostCommonReference(
|
||||||
|
Source,
|
||||||
|
ExpectedMostCommon,
|
||||||
|
ExpectedMostCommonCount);
|
||||||
|
int32 MostCommon = 0;
|
||||||
|
int32 MostCommonCount = 0;
|
||||||
|
const bool bFoundMostCommon = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetMostCommon(
|
||||||
|
&Source,
|
||||||
|
ArrayProperty,
|
||||||
|
&MostCommon,
|
||||||
|
&MostCommonCount);
|
||||||
|
TestEqual(Label + TEXT(" GetMostCommon result"), bFoundMostCommon, bExpectedMostCommon);
|
||||||
|
TestEqual(Label + TEXT(" GetMostCommon value"), MostCommon, ExpectedMostCommon);
|
||||||
|
TestEqual(Label + TEXT(" GetMostCommon count"), MostCommonCount, ExpectedMostCommonCount);
|
||||||
|
|
||||||
|
const int32 QueryValue = Stream.RandRange(-70, 70);
|
||||||
|
TestEqual(
|
||||||
|
Label + TEXT(" CountOccurrences"),
|
||||||
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_CountOccurrences(&Source, ArrayProperty, &QueryValue),
|
||||||
|
CountReference(Source, QueryValue));
|
||||||
|
|
||||||
|
const TArray<int32> ExpectedRemoved = RemoveAllReference(Source, QueryValue);
|
||||||
|
TestObject->TestArray = Source;
|
||||||
|
const bool bRemoved = UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAllOccurrences(
|
||||||
|
&TestObject->TestArray,
|
||||||
|
ArrayProperty,
|
||||||
|
&QueryValue);
|
||||||
|
TestEqual(Label + TEXT(" RemoveAllOccurrences result"), bRemoved, ExpectedRemoved.Num() != Source.Num());
|
||||||
|
TestEqual(Label + TEXT(" RemoveAllOccurrences values"), TestObject->TestArray, ExpectedRemoved);
|
||||||
|
|
||||||
|
TArray<int32> ExpectedAppended = Source;
|
||||||
|
ExpectedAppended.Append(Source);
|
||||||
|
TestObject->TestArray = Source;
|
||||||
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_AppendOptimized(
|
||||||
|
&TestObject->TestArray,
|
||||||
|
ArrayProperty,
|
||||||
|
&TestObject->TestArray,
|
||||||
|
ArrayProperty);
|
||||||
|
TestEqual(Label + TEXT(" AppendOptimized self append"), TestObject->TestArray, ExpectedAppended);
|
||||||
|
|
||||||
|
for (const int32 Shift : {MIN_int32, -ItemCount - 1, -1, 0, 1, ItemCount + 1, MAX_int32})
|
||||||
|
{
|
||||||
|
TestObject->TestArray = Source;
|
||||||
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Rotate(&TestObject->TestArray, ArrayProperty, Shift);
|
||||||
|
TestEqual(
|
||||||
|
Label + FString::Printf(TEXT(" Rotate shift=%d"), Shift),
|
||||||
|
TestObject->TestArray,
|
||||||
|
MakeRotationReference(Source, Shift));
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 StartIndex = Stream.RandRange(-ItemCount - 2, ItemCount + 2);
|
||||||
|
const int32 SliceCount = Stream.RandRange(-2, ItemCount + 2);
|
||||||
|
const int32 ExpectedStart = FMath::Clamp(StartIndex, 0, ItemCount);
|
||||||
|
const int32 ExpectedSliceCount = FMath::Max(0, FMath::Min(SliceCount, ItemCount - ExpectedStart));
|
||||||
|
const TArray<int32> ExpectedSlice = SliceReference(Source, ExpectedStart, ExpectedSliceCount);
|
||||||
|
TArray<int32> SliceResult;
|
||||||
|
UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(
|
||||||
|
&Source,
|
||||||
|
ArrayProperty,
|
||||||
|
StartIndex,
|
||||||
|
SliceCount,
|
||||||
|
&SliceResult,
|
||||||
|
ArrayProperty);
|
||||||
|
TestEqual(Label + TEXT(" Slice"), SliceResult, ExpectedSlice);
|
||||||
|
|
||||||
|
const int32 PageSize = CountIndex % 4 == 0 ? 0 : 1 << (CountIndex % 7);
|
||||||
|
const int32 ExpectedPageCount = PageSize > 0 ? FMath::DivideAndRoundUp(ItemCount, PageSize) : 0;
|
||||||
|
for (const int32 PageIndex : {-1, 0, FMath::Max(0, ExpectedPageCount - 1), ExpectedPageCount})
|
||||||
|
{
|
||||||
|
TArray<int32> PageResult;
|
||||||
|
int32 PageCount = -1;
|
||||||
|
const bool bPageValid = UDirectiveUtilArrayFunctionLibrary::GenericArray_GetPage(
|
||||||
|
&Source,
|
||||||
|
ArrayProperty,
|
||||||
|
PageIndex,
|
||||||
|
PageSize,
|
||||||
|
&PageResult,
|
||||||
|
ArrayProperty,
|
||||||
|
&PageCount);
|
||||||
|
const bool bExpectedValid = PageSize > 0 && PageIndex >= 0 && PageIndex < ExpectedPageCount;
|
||||||
|
const TArray<int32> ExpectedPage = bExpectedValid
|
||||||
|
? SliceReference(Source, PageIndex * PageSize, FMath::Min(PageSize, ItemCount - PageIndex * PageSize))
|
||||||
|
: TArray<int32>();
|
||||||
|
const FString PageLabel = Label + FString::Printf(TEXT(" Page index=%d size=%d"), PageIndex, PageSize);
|
||||||
|
TestEqual(PageLabel + TEXT(" validity"), bPageValid, bExpectedValid);
|
||||||
|
TestEqual(
|
||||||
|
PageLabel + TEXT(" count"),
|
||||||
|
PageCount,
|
||||||
|
PageSize > 0 && PageIndex >= 0 ? ExpectedPageCount : 0);
|
||||||
|
TestEqual(PageLabel + TEXT(" values"), PageResult, ExpectedPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -12,13 +12,11 @@
|
|||||||
#include "Components/StaticMeshComponent.h"
|
#include "Components/StaticMeshComponent.h"
|
||||||
#include "GameFramework/DefaultPawn.h"
|
#include "GameFramework/DefaultPawn.h"
|
||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
|
#include "TimerManager.h"
|
||||||
#include "Misc/AutomationTest.h"
|
#include "Misc/AutomationTest.h"
|
||||||
|
|
||||||
#if WITH_EDITOR
|
|
||||||
|
|
||||||
namespace DirectiveUtilAsyncTraceTestHelpers
|
namespace DirectiveUtilAsyncTraceTestHelpers
|
||||||
{
|
{
|
||||||
/** Creates a transient game world with a physics scene, initialized for play so traces resolve. */
|
|
||||||
UWorld* CreateTraceWorld()
|
UWorld* CreateTraceWorld()
|
||||||
{
|
{
|
||||||
UWorld* World = UWorld::CreateWorld(EWorldType::Game, false);
|
UWorld* World = UWorld::CreateWorld(EWorldType::Game, false);
|
||||||
@@ -33,7 +31,6 @@ namespace DirectiveUtilAsyncTraceTestHelpers
|
|||||||
return World;
|
return World;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Spawns a blocking cube actor at the origin so traces have something to hit. */
|
|
||||||
AStaticMeshActor* SpawnBlockingCube(UWorld* World, UStaticMesh* CubeMesh)
|
AStaticMeshActor* SpawnBlockingCube(UWorld* World, UStaticMesh* CubeMesh)
|
||||||
{
|
{
|
||||||
AStaticMeshActor* Cube = World->SpawnActor<AStaticMeshActor>(FVector::ZeroVector, FRotator::ZeroRotator);
|
AStaticMeshActor* Cube = World->SpawnActor<AStaticMeshActor>(FVector::ZeroVector, FRotator::ZeroRotator);
|
||||||
@@ -42,15 +39,36 @@ namespace DirectiveUtilAsyncTraceTestHelpers
|
|||||||
Component->SetStaticMesh(CubeMesh);
|
Component->SetStaticMesh(CubeMesh);
|
||||||
Component->SetCollisionProfileName(TEXT("BlockAll"));
|
Component->SetCollisionProfileName(TEXT("BlockAll"));
|
||||||
Component->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
|
Component->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
|
||||||
Component->UpdateCollisionProfile();
|
|
||||||
return Cube;
|
return Cube;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
bool UDirectiveUtilTestMoveToLocationTask::HasRegisteredTimers() const
|
||||||
* 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.
|
const UWorld* World = TimerWorld.Get();
|
||||||
*/
|
return World && (World->GetTimerManager().TimerExists(TimerHandle) || World->GetTimerManager().TimerExists(StuckTimerHandle));
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilTestMoveToLocationTask::RegisterTimersForTest(UWorld* World)
|
||||||
|
{
|
||||||
|
TimerWorld = World;
|
||||||
|
World->GetTimerManager().SetTimer(TimerHandle, FTimerDelegate::CreateLambda([] {}), 60.0f, true);
|
||||||
|
World->GetTimerManager().SetTimer(StuckTimerHandle, FTimerDelegate::CreateLambda([] {}), 60.0f, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UDirectiveUtilTestMoveToActorTask::HasRegisteredTimers() const
|
||||||
|
{
|
||||||
|
const UWorld* World = TimerWorld.Get();
|
||||||
|
return World && (World->GetTimerManager().TimerExists(TimerHandle) || World->GetTimerManager().TimerExists(StuckTimerHandle));
|
||||||
|
}
|
||||||
|
|
||||||
|
void UDirectiveUtilTestMoveToActorTask::RegisterTimersForTest(UWorld* World)
|
||||||
|
{
|
||||||
|
TimerWorld = World;
|
||||||
|
World->GetTimerManager().SetTimer(TimerHandle, FTimerDelegate::CreateLambda([] {}), 60.0f, true);
|
||||||
|
World->GetTimerManager().SetTimer(StuckTimerHandle, FTimerDelegate::CreateLambda([] {}), 60.0f, true);
|
||||||
|
}
|
||||||
|
|
||||||
class FDirectiveUtilTickTraceWorld : public IAutomationLatentCommand
|
class FDirectiveUtilTickTraceWorld : public IAutomationLatentCommand
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -111,18 +129,12 @@ private:
|
|||||||
int32 FramesRemaining;
|
int32 FramesRemaining;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilAsyncTraceTest, "DirectiveUtilities.AsyncTaskTraceTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
* 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)
|
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);
|
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>();
|
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
|
||||||
Listener->AddToRoot();
|
Listener->AddToRoot();
|
||||||
@@ -142,8 +154,8 @@ bool FDirectiveUtilAsyncTraceTest::RunTest(const FString& Parameters)
|
|||||||
UStaticMesh* CubeMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
|
UStaticMesh* CubeMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
|
||||||
if (!CubeMesh)
|
if (!CubeMesh)
|
||||||
{
|
{
|
||||||
AddInfo(TEXT("Engine cube mesh unavailable; skipping async trace hit scenarios."));
|
AddError(TEXT("Engine cube mesh unavailable for async trace hit scenarios."));
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
UWorld* World = DirectiveUtilAsyncTraceTestHelpers::CreateTraceWorld();
|
UWorld* World = DirectiveUtilAsyncTraceTestHelpers::CreateTraceWorld();
|
||||||
@@ -154,10 +166,9 @@ bool FDirectiveUtilAsyncTraceTest::RunTest(const FString& Parameters)
|
|||||||
}
|
}
|
||||||
DirectiveUtilAsyncTraceTestHelpers::SpawnBlockingCube(World, CubeMesh);
|
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 Start(0.0f, 0.0f, 500.0f);
|
||||||
const FVector End(0.0f, 0.0f, -500.0f);
|
const FVector End(0.0f, 0.0f, -500.0f);
|
||||||
const ETraceTypeQuery Channel = ETraceTypeQuery::TraceTypeQuery1; // Visibility, which BlockAll blocks.
|
const ETraceTypeQuery VisibilityChannel = ETraceTypeQuery::TraceTypeQuery1;
|
||||||
|
|
||||||
auto MakeListener = [](UDirectiveUtilTask_AsyncTrace* Task) -> UDirectiveUtilDelegateListener*
|
auto MakeListener = [](UDirectiveUtilTask_AsyncTrace* Task) -> UDirectiveUtilDelegateListener*
|
||||||
{
|
{
|
||||||
@@ -170,82 +181,23 @@ bool FDirectiveUtilAsyncTraceTest::RunTest(const FString& Parameters)
|
|||||||
};
|
};
|
||||||
|
|
||||||
TArray<UDirectiveUtilDelegateListener*> Listeners;
|
TArray<UDirectiveUtilDelegateListener*> Listeners;
|
||||||
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncLineTraceByChannel(World, Start, End, Channel, false)));
|
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncLineTraceByChannel(World, Start, End, VisibilityChannel, false)));
|
||||||
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncSphereTraceByChannel(World, Start, End, 25.0f, Channel, false)));
|
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncSphereTraceByChannel(World, Start, End, 25.0f, VisibilityChannel, false)));
|
||||||
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncBoxTraceByChannel(World, Start, End, FVector(25.0f), FRotator::ZeroRotator, Channel, false)));
|
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncBoxTraceByChannel(World, Start, End, FVector(25.0f), FRotator::ZeroRotator, VisibilityChannel, false)));
|
||||||
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncCapsuleTraceByChannel(World, Start, End, 25.0f, 50.0f, Channel, false)));
|
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncCapsuleTraceByChannel(World, Start, End, 25.0f, 50.0f, VisibilityChannel, false)));
|
||||||
|
|
||||||
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickTraceWorld(this, World, Listeners, 120));
|
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickTraceWorld(this, World, Listeners, 120));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMoveToLocationTest, "DirectiveUtilities.AsyncTaskMoveToLocationTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
* 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)
|
bool FDirectiveUtilMoveToLocationTest::RunTest(const FString& Parameters)
|
||||||
{
|
{
|
||||||
// The guard paths intentionally log a warning.
|
AddExpectedMessagePlain(TEXT("Controller, pawn, or world is unavailable while moving to location. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||||
AddExpectedMessagePlain(TEXT("Controller or pawn has been destroyed while moving to location. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
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>();
|
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
|
||||||
Listener->AddToRoot();
|
Listener->AddToRoot();
|
||||||
@@ -262,16 +214,15 @@ bool FDirectiveUtilMoveToLocationTest::RunTest(const FString& Parameters)
|
|||||||
Listener->RemoveFromRoot();
|
Listener->RemoveFromRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
|
UWorld* World = UWorld::CreateWorld(EWorldType::Game, false);
|
||||||
if (!World)
|
if (!World)
|
||||||
{
|
{
|
||||||
AddError(TEXT("Failed to create a transient world for the move-to-location test."));
|
AddError(TEXT("Failed to create a transient world for the move-to-location test."));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor);
|
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Game);
|
||||||
WorldContext.SetCurrentWorld(World);
|
WorldContext.SetCurrentWorld(World);
|
||||||
|
|
||||||
// Controller-without-pawn guard: activating broadcasts Completed(false).
|
|
||||||
{
|
{
|
||||||
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
||||||
|
|
||||||
@@ -290,7 +241,6 @@ bool FDirectiveUtilMoveToLocationTest::RunTest(const FString& Parameters)
|
|||||||
Listener->RemoveFromRoot();
|
Listener->RemoveFromRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
// EndTask broadcasts Completed(false) and clears timers without crashing.
|
|
||||||
{
|
{
|
||||||
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
||||||
|
|
||||||
@@ -309,7 +259,6 @@ bool FDirectiveUtilMoveToLocationTest::RunTest(const FString& Parameters)
|
|||||||
Listener->RemoveFromRoot();
|
Listener->RemoveFromRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Double completion guard: a second EndTask does not broadcast Completed again.
|
|
||||||
{
|
{
|
||||||
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
||||||
|
|
||||||
@@ -328,31 +277,44 @@ bool FDirectiveUtilMoveToLocationTest::RunTest(const FString& Parameters)
|
|||||||
Listener->RemoveFromRoot();
|
Listener->RemoveFromRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
||||||
|
ADefaultPawn* Pawn = World->SpawnActor<ADefaultPawn>();
|
||||||
|
if (Controller && Pawn)
|
||||||
|
{
|
||||||
|
Controller->SetPawn(Pawn);
|
||||||
|
UDirectiveUtilTestMoveToLocationTask* Task = NewObject<UDirectiveUtilTestMoveToLocationTask>();
|
||||||
|
Task->Configure(Controller, FVector(1000.0f, 0.0f, 0.0f), true);
|
||||||
|
Task->RegisterTimersForTest(World);
|
||||||
|
TestTrue("Move to location registers both lifecycle timers", Task->HasRegisteredTimers());
|
||||||
|
Task->ClearController();
|
||||||
|
Task->Complete();
|
||||||
|
TestFalse("Move to location clears timers without a controller", Task->HasRegisteredTimers());
|
||||||
|
Task->Configure(Controller, FVector(1000.0f, 0.0f, 0.0f), true);
|
||||||
|
Task->Activate();
|
||||||
|
TestFalse("A completed move to location should not restart", Task->HasRegisteredTimers());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
GEngine->DestroyWorldContext(World);
|
GEngine->DestroyWorldContext(World);
|
||||||
World->DestroyWorld(false);
|
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();
|
UWorld* MoveWorld = DirectiveUtilAsyncTraceTestHelpers::CreateTraceWorld();
|
||||||
if (!MoveWorld)
|
if (!MoveWorld)
|
||||||
{
|
{
|
||||||
AddInfo(TEXT("Failed to create a transient game world; skipping the no-navigation move scenario."));
|
AddError(TEXT("Failed to create a transient game world for the no-navigation move scenario."));
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
APlayerController* Controller = MoveWorld->SpawnActor<APlayerController>();
|
APlayerController* Controller = MoveWorld->SpawnActor<APlayerController>();
|
||||||
ADefaultPawn* Pawn = MoveWorld->SpawnActor<ADefaultPawn>(FVector::ZeroVector, FRotator::ZeroRotator);
|
ADefaultPawn* Pawn = MoveWorld->SpawnActor<ADefaultPawn>(FVector::ZeroVector, FRotator::ZeroRotator);
|
||||||
if (!Controller || !Pawn)
|
if (!Controller || !Pawn)
|
||||||
{
|
{
|
||||||
AddInfo(TEXT("Failed to spawn a controller or pawn; skipping the no-navigation move scenario."));
|
AddError(TEXT("Failed to spawn a controller or pawn for the no-navigation move scenario."));
|
||||||
GEngine->DestroyWorldContext(MoveWorld);
|
GEngine->DestroyWorldContext(MoveWorld);
|
||||||
MoveWorld->DestroyWorld(false);
|
MoveWorld->DestroyWorld(false);
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
Controller->SetPawn(Pawn);
|
Controller->SetPawn(Pawn);
|
||||||
|
|
||||||
@@ -364,26 +326,26 @@ bool FDirectiveUtilMoveToLocationTest::RunTest(const FString& Parameters)
|
|||||||
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
|
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
|
||||||
Task->Activate();
|
Task->Activate();
|
||||||
|
|
||||||
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickMoveToLocationWorld(this, MoveWorld, Listener, 120));
|
TestTrue("A location move without navigation broadcasts Completed", Listener->bCompleted);
|
||||||
|
TestFalse("A location move without navigation reports failure", Listener->bLastSuccess);
|
||||||
|
TestEqual("A location move without navigation completes exactly once", Listener->CompletedCount, 1);
|
||||||
|
Listener->Keepalive = nullptr;
|
||||||
|
Listener->RemoveFromRoot();
|
||||||
|
GEngine->DestroyWorldContext(MoveWorld);
|
||||||
|
MoveWorld->DestroyWorld(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMoveToActorTest, "DirectiveUtilities.AsyncTaskMoveToActorTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
* 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)
|
bool FDirectiveUtilMoveToActorTest::RunTest(const FString& Parameters)
|
||||||
{
|
{
|
||||||
// The guard paths intentionally log a warning.
|
AddExpectedMessagePlain(TEXT("Controller, pawn, goal, or world is unavailable while moving to actor. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||||
|
AddExpectedMessagePlain(TEXT("Controller, pawn, or world is unavailable while moving to actor. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||||
AddExpectedMessagePlain(TEXT("Controller, pawn, or goal has been destroyed while moving to actor. Aborting."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
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>();
|
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
|
||||||
Listener->AddToRoot();
|
Listener->AddToRoot();
|
||||||
@@ -400,16 +362,15 @@ bool FDirectiveUtilMoveToActorTest::RunTest(const FString& Parameters)
|
|||||||
Listener->RemoveFromRoot();
|
Listener->RemoveFromRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
|
UWorld* World = UWorld::CreateWorld(EWorldType::Game, false);
|
||||||
if (!World)
|
if (!World)
|
||||||
{
|
{
|
||||||
AddError(TEXT("Failed to create a transient world for the move-to-actor test."));
|
AddError(TEXT("Failed to create a transient world for the move-to-actor test."));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor);
|
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Game);
|
||||||
WorldContext.SetCurrentWorld(World);
|
WorldContext.SetCurrentWorld(World);
|
||||||
|
|
||||||
// Null goal guard: a controller with a pawn but no goal broadcasts Completed(false).
|
|
||||||
{
|
{
|
||||||
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
||||||
ADefaultPawn* Pawn = World->SpawnActor<ADefaultPawn>(FVector::ZeroVector, FRotator::ZeroRotator);
|
ADefaultPawn* Pawn = World->SpawnActor<ADefaultPawn>(FVector::ZeroVector, FRotator::ZeroRotator);
|
||||||
@@ -433,11 +394,10 @@ bool FDirectiveUtilMoveToActorTest::RunTest(const FString& Parameters)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
AddInfo(TEXT("Failed to spawn a controller or pawn; skipping the null-goal scenario."));
|
AddError(TEXT("Failed to spawn a controller or pawn for the null-goal scenario."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Double completion guard: a second EndTask does not broadcast Completed again.
|
|
||||||
{
|
{
|
||||||
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
||||||
|
|
||||||
@@ -456,21 +416,35 @@ bool FDirectiveUtilMoveToActorTest::RunTest(const FString& Parameters)
|
|||||||
Listener->RemoveFromRoot();
|
Listener->RemoveFromRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
||||||
|
ADefaultPawn* Pawn = World->SpawnActor<ADefaultPawn>();
|
||||||
|
AStaticMeshActor* Goal = World->SpawnActor<AStaticMeshActor>();
|
||||||
|
if (Controller && Pawn && Goal)
|
||||||
|
{
|
||||||
|
Controller->SetPawn(Pawn);
|
||||||
|
UDirectiveUtilTestMoveToActorTask* Task = NewObject<UDirectiveUtilTestMoveToActorTask>();
|
||||||
|
Task->Configure(Controller, Goal, true);
|
||||||
|
Task->RegisterTimersForTest(World);
|
||||||
|
TestTrue("Move to actor registers both lifecycle timers", Task->HasRegisteredTimers());
|
||||||
|
Task->ClearController();
|
||||||
|
Task->Complete();
|
||||||
|
TestFalse("Move to actor clears timers without a controller", Task->HasRegisteredTimers());
|
||||||
|
Task->Configure(Controller, Goal, true);
|
||||||
|
Task->Activate();
|
||||||
|
TestFalse("A completed move to actor should not restart", Task->HasRegisteredTimers());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
GEngine->DestroyWorldContext(World);
|
GEngine->DestroyWorldContext(World);
|
||||||
World->DestroyWorld(false);
|
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();
|
UWorld* MoveWorld = DirectiveUtilAsyncTraceTestHelpers::CreateTraceWorld();
|
||||||
if (!MoveWorld)
|
if (!MoveWorld)
|
||||||
{
|
{
|
||||||
AddInfo(TEXT("Failed to create a transient game world; skipping the no-navigation move scenario."));
|
AddError(TEXT("Failed to create a transient game world for the no-navigation move scenario."));
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
APlayerController* Controller = MoveWorld->SpawnActor<APlayerController>();
|
APlayerController* Controller = MoveWorld->SpawnActor<APlayerController>();
|
||||||
@@ -478,10 +452,10 @@ bool FDirectiveUtilMoveToActorTest::RunTest(const FString& Parameters)
|
|||||||
AStaticMeshActor* GoalActor = MoveWorld->SpawnActor<AStaticMeshActor>(FVector(10000.0f, 0.0f, 0.0f), FRotator::ZeroRotator);
|
AStaticMeshActor* GoalActor = MoveWorld->SpawnActor<AStaticMeshActor>(FVector(10000.0f, 0.0f, 0.0f), FRotator::ZeroRotator);
|
||||||
if (!Controller || !Pawn || !GoalActor)
|
if (!Controller || !Pawn || !GoalActor)
|
||||||
{
|
{
|
||||||
AddInfo(TEXT("Failed to spawn a controller, pawn, or goal; skipping the no-navigation move scenario."));
|
AddError(TEXT("Failed to spawn a controller, pawn, or goal for the no-navigation move scenario."));
|
||||||
GEngine->DestroyWorldContext(MoveWorld);
|
GEngine->DestroyWorldContext(MoveWorld);
|
||||||
MoveWorld->DestroyWorld(false);
|
MoveWorld->DestroyWorld(false);
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
Controller->SetPawn(Pawn);
|
Controller->SetPawn(Pawn);
|
||||||
|
|
||||||
@@ -493,10 +467,14 @@ bool FDirectiveUtilMoveToActorTest::RunTest(const FString& Parameters)
|
|||||||
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
|
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
|
||||||
Task->Activate();
|
Task->Activate();
|
||||||
|
|
||||||
ADD_LATENT_AUTOMATION_COMMAND(FDirectiveUtilTickMoveToLocationWorld(this, MoveWorld, Listener, 120));
|
TestTrue("An actor move without navigation broadcasts Completed", Listener->bCompleted);
|
||||||
|
TestFalse("An actor move without navigation reports failure", Listener->bLastSuccess);
|
||||||
|
TestEqual("An actor move without navigation completes exactly once", Listener->CompletedCount, 1);
|
||||||
|
Listener->Keepalive = nullptr;
|
||||||
|
Listener->RemoveFromRoot();
|
||||||
|
GEngine->DestroyWorldContext(MoveWorld);
|
||||||
|
MoveWorld->DestroyWorld(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // WITH_EDITOR
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
#include "Misc/AutomationTest.h"
|
#include "Misc/AutomationTest.h"
|
||||||
|
#include "UObject/Class.h"
|
||||||
|
#include "UObject/Package.h"
|
||||||
|
#include "UObject/UnrealType.h"
|
||||||
#include "UObject/UObjectIterator.h"
|
#include "UObject/UObjectIterator.h"
|
||||||
|
|
||||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilBlueprintCategoryTest, "DirectiveUtilities.BlueprintCategoryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilBlueprintCategoryTest, "DirectiveUtilities.BlueprintCategoryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||||
@@ -8,8 +11,9 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilBlueprintCategoryTest, "Directive
|
|||||||
bool FDirectiveUtilBlueprintCategoryTest::RunTest(const FString& Parameters)
|
bool FDirectiveUtilBlueprintCategoryTest::RunTest(const FString& Parameters)
|
||||||
{
|
{
|
||||||
const FString ExpectedCategoryRoot = TEXT("Directive Utilities");
|
const FString ExpectedCategoryRoot = TEXT("Directive Utilities");
|
||||||
|
const FString RuntimeScriptPackage = TEXT("/Script/DirectiveUtilitiesRuntime");
|
||||||
const TSet<FString> PluginScriptPackages = {
|
const TSet<FString> PluginScriptPackages = {
|
||||||
TEXT("/Script/DirectiveUtilitiesRuntime"),
|
RuntimeScriptPackage,
|
||||||
TEXT("/Script/DirectiveUtilitiesEditor")
|
TEXT("/Script/DirectiveUtilitiesEditor")
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,6 +36,15 @@ bool FDirectiveUtilBlueprintCategoryTest::RunTest(const FString& Parameters)
|
|||||||
}
|
}
|
||||||
|
|
||||||
++TestedFunctionCount;
|
++TestedFunctionCount;
|
||||||
|
if (Class->GetOutermost()->GetName() == RuntimeScriptPackage)
|
||||||
|
{
|
||||||
|
TestFalse(
|
||||||
|
FString::Printf(TEXT("%s.%s is available at runtime"), *Class->GetName(), *Function->GetName()),
|
||||||
|
Class->GetOutermost()->HasAnyPackageFlags(PKG_EditorOnly | PKG_UncookedOnly | PKG_Developer)
|
||||||
|
|| Function->HasAnyFunctionFlags(FUNC_EditorOnly)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const FString Category = Function->GetMetaData(TEXT("Category"));
|
const FString Category = Function->GetMetaData(TEXT("Category"));
|
||||||
FString CategoryRoot = Category;
|
FString CategoryRoot = Category;
|
||||||
int32 CategoryDelimiterIndex = INDEX_NONE;
|
int32 CategoryDelimiterIndex = INDEX_NONE;
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
|
||||||
|
#include "AssetRegistry/DirectiveUtilDependencyCycleFinder.h"
|
||||||
|
#include "Misc/AutomationTest.h"
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
|
FDirectiveUtilDependencyCycleFinderTest,
|
||||||
|
"DirectiveUtilities.EditorDependencyCycleFinderTests",
|
||||||
|
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FDirectiveUtilDependencyCycleFinderTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
TMap<FName, TArray<FName>> Graph;
|
||||||
|
Graph.Add(TEXT("/Game/A"), {TEXT("/Game/B")});
|
||||||
|
Graph.Add(TEXT("/Game/B"), {TEXT("/Game/A")});
|
||||||
|
Graph.Add(TEXT("/Game/C"), {TEXT("/Game/C")});
|
||||||
|
Graph.Add(TEXT("/Game/D"), {TEXT("/Game/E")});
|
||||||
|
Graph.Add(TEXT("/Game/E"));
|
||||||
|
|
||||||
|
const TArray<FDirectiveUtilAssetDependencyCycle> Cycles =
|
||||||
|
DirectiveUtilitiesEditor::FDependencyCycleFinder(Graph).Find();
|
||||||
|
TestEqual(TEXT("The graph contains two cycles"), Cycles.Num(), 2);
|
||||||
|
if (Cycles.Num() == 2)
|
||||||
|
{
|
||||||
|
TestEqual(TEXT("The first cycle contains two packages"), Cycles[0].Packages.Num(), 2);
|
||||||
|
TestTrue(TEXT("The first cycle contains A"), Cycles[0].Packages.Contains(TEXT("/Game/A")));
|
||||||
|
TestTrue(TEXT("The first cycle contains B"), Cycles[0].Packages.Contains(TEXT("/Game/B")));
|
||||||
|
TestEqual(TEXT("The self-cycle contains one package"), Cycles[1].Packages.Num(), 1);
|
||||||
|
TestTrue(TEXT("The self-cycle contains C"), Cycles[1].Packages.Contains(TEXT("/Game/C")));
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int32 NodeCount = 20000;
|
||||||
|
constexpr int32 CycleStart = NodeCount / 2;
|
||||||
|
TArray<FName> Nodes;
|
||||||
|
Nodes.Reserve(NodeCount);
|
||||||
|
for (int32 Index = 0; Index < NodeCount; ++Index)
|
||||||
|
{
|
||||||
|
Nodes.Add(*FString::Printf(TEXT("/Game/Deep/%05d"), Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
TMap<FName, TArray<FName>> DeepGraph;
|
||||||
|
DeepGraph.Reserve(NodeCount);
|
||||||
|
for (int32 Index = 0; Index < NodeCount - 1; ++Index)
|
||||||
|
{
|
||||||
|
DeepGraph.Add(Nodes[Index], {Nodes[Index + 1]});
|
||||||
|
}
|
||||||
|
DeepGraph.Add(Nodes.Last(), {Nodes[CycleStart]});
|
||||||
|
|
||||||
|
const TArray<FDirectiveUtilAssetDependencyCycle> DeepCycles =
|
||||||
|
DirectiveUtilitiesEditor::FDependencyCycleFinder(DeepGraph).Find();
|
||||||
|
TestEqual(TEXT("The deep graph contains one cycle"), DeepCycles.Num(), 1);
|
||||||
|
if (DeepCycles.Num() == 1)
|
||||||
|
{
|
||||||
|
TestEqual(TEXT("The deep cycle contains every connected member"), DeepCycles[0].Packages.Num(), NodeCount - CycleStart);
|
||||||
|
TestTrue(TEXT("The deep cycle starts at the expected package"), DeepCycles[0].Packages.Contains(Nodes[CycleStart]));
|
||||||
|
TestTrue(TEXT("The deep cycle ends at the expected package"), DeepCycles[0].Packages.Contains(Nodes.Last()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
|
||||||
|
#include "Subsystems/DirectiveUtilEditorActorSubsystem.h"
|
||||||
|
#include "Components/BoxComponent.h"
|
||||||
|
#include "Engine/Engine.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "Misc/AutomationTest.h"
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
|
FDirectiveUtilEditorActorLayoutTest,
|
||||||
|
"DirectiveUtilities.EditorActorLayoutTests",
|
||||||
|
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FDirectiveUtilEditorActorLayoutTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
|
||||||
|
if (!World)
|
||||||
|
{
|
||||||
|
AddError(TEXT("Failed to create an editor world"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor);
|
||||||
|
WorldContext.SetCurrentWorld(World);
|
||||||
|
|
||||||
|
auto SpawnBox = [World](const FVector Location, const FVector Extent, const ECollisionChannel ObjectType) {
|
||||||
|
AActor* Actor = World->SpawnActor<AActor>();
|
||||||
|
Actor->ClearFlags(RF_Transient);
|
||||||
|
UBoxComponent* Box = NewObject<UBoxComponent>(Actor);
|
||||||
|
Actor->SetRootComponent(Box);
|
||||||
|
Box->SetBoxExtent(Extent);
|
||||||
|
Box->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||||
|
Box->SetCollisionObjectType(ObjectType);
|
||||||
|
Box->SetCollisionResponseToAllChannels(ECR_Block);
|
||||||
|
Box->RegisterComponent();
|
||||||
|
Actor->SetActorLocation(Location);
|
||||||
|
return Actor;
|
||||||
|
};
|
||||||
|
|
||||||
|
AActor* First = SpawnBox(FVector(0.0f, 0.0f, 100.0f), FVector(10.0f), ECC_WorldDynamic);
|
||||||
|
AActor* Middle = SpawnBox(FVector(30.0f, 20.0f, 100.0f), FVector(10.0f), ECC_WorldDynamic);
|
||||||
|
AActor* Last = SpawnBox(FVector(100.0f, 40.0f, 100.0f), FVector(10.0f), ECC_WorldDynamic);
|
||||||
|
|
||||||
|
FDirectiveUtilActorOperationResult AlignResult = UDirectiveUtilEditorActorSubsystem::AlignActors(
|
||||||
|
{First, Middle, Last},
|
||||||
|
EDirectiveUtilActorLayoutAxis::Y,
|
||||||
|
EDirectiveUtilActorAlignment::Minimum);
|
||||||
|
TestEqual(TEXT("Alignment changes two actors"), AlignResult.ChangedActors.Num(), 2);
|
||||||
|
TestTrue(TEXT("Alignment preserves X"), FMath::IsNearlyEqual(Middle->GetActorLocation().X, 30.0f));
|
||||||
|
TestTrue(TEXT("Alignment matches minimum bounds"), FMath::IsNearlyEqual(Middle->GetActorLocation().Y, 0.0f));
|
||||||
|
|
||||||
|
FDirectiveUtilActorOperationResult DistributeResult = UDirectiveUtilEditorActorSubsystem::DistributeActors(
|
||||||
|
{First, Middle, Last},
|
||||||
|
EDirectiveUtilActorLayoutAxis::X,
|
||||||
|
EDirectiveUtilActorDistribution::Centers);
|
||||||
|
TestEqual(TEXT("Distribution changes the middle actor"), DistributeResult.ChangedActors.Num(), 1);
|
||||||
|
TestTrue(TEXT("Center distribution uses equal spacing"), FMath::IsNearlyEqual(Middle->GetActorLocation().X, 50.0f));
|
||||||
|
TestTrue(TEXT("Distribution preserves Y"), FMath::IsNearlyEqual(Middle->GetActorLocation().Y, 0.0f));
|
||||||
|
|
||||||
|
AActor* Floor = SpawnBox(FVector::ZeroVector, FVector(200.0f, 200.0f, 10.0f), ECC_WorldStatic);
|
||||||
|
World->UpdateWorldComponents(true, false);
|
||||||
|
const FVector BeforeInvalidSnap = Middle->GetActorLocation();
|
||||||
|
const FDirectiveUtilActorOperationResult InvalidDistanceResult = UDirectiveUtilEditorActorSubsystem::SnapActorsToSurface(
|
||||||
|
{Middle},
|
||||||
|
FVector::DownVector,
|
||||||
|
std::numeric_limits<float>::quiet_NaN(),
|
||||||
|
ECC_Visibility,
|
||||||
|
EDirectiveUtilSurfacePlacement::Pivot,
|
||||||
|
false);
|
||||||
|
TestTrue(TEXT("Surface snapping skips a non-finite distance"), InvalidDistanceResult.SkippedActors.Contains(Middle));
|
||||||
|
TestTrue(TEXT("A non-finite distance preserves the actor transform"), Middle->GetActorLocation().Equals(BeforeInvalidSnap));
|
||||||
|
|
||||||
|
const FDirectiveUtilActorOperationResult InvalidDirectionResult = UDirectiveUtilEditorActorSubsystem::SnapActorsToSurface(
|
||||||
|
{Middle},
|
||||||
|
FVector(std::numeric_limits<float>::infinity(), 0.0f, -1.0f),
|
||||||
|
500.0f,
|
||||||
|
ECC_Visibility,
|
||||||
|
EDirectiveUtilSurfacePlacement::Pivot,
|
||||||
|
false);
|
||||||
|
TestTrue(TEXT("Surface snapping skips a non-finite direction"), InvalidDirectionResult.SkippedActors.Contains(Middle));
|
||||||
|
|
||||||
|
const FDirectiveUtilActorOperationResult InvalidChannelResult = UDirectiveUtilEditorActorSubsystem::SnapActorsToSurface(
|
||||||
|
{Middle},
|
||||||
|
FVector::DownVector,
|
||||||
|
500.0f,
|
||||||
|
static_cast<ECollisionChannel>(ECC_MAX),
|
||||||
|
EDirectiveUtilSurfacePlacement::Pivot,
|
||||||
|
false);
|
||||||
|
TestTrue(TEXT("Surface snapping skips an invalid collision channel"), InvalidChannelResult.SkippedActors.Contains(Middle));
|
||||||
|
|
||||||
|
FDirectiveUtilActorOperationResult SnapResult = UDirectiveUtilEditorActorSubsystem::SnapActorsToSurface(
|
||||||
|
{Middle},
|
||||||
|
FVector::DownVector,
|
||||||
|
500.0f,
|
||||||
|
ECC_Visibility,
|
||||||
|
EDirectiveUtilSurfacePlacement::Pivot,
|
||||||
|
false);
|
||||||
|
TestEqual(TEXT("Surface snapping changes the actor"), SnapResult.ChangedActors.Num(), 1);
|
||||||
|
TestTrue(TEXT("Pivot snapping reaches the floor surface"), FMath::IsNearlyEqual(Middle->GetActorLocation().Z, 10.0f, 0.1f));
|
||||||
|
|
||||||
|
AActor* BoundsActor = SpawnBox(FVector(150.0f, 150.0f, 100.0f), FVector(10.0f), ECC_WorldDynamic);
|
||||||
|
FDirectiveUtilActorOperationResult BoundsSnapResult = UDirectiveUtilEditorActorSubsystem::SnapActorsToSurface(
|
||||||
|
{BoundsActor},
|
||||||
|
FVector::DownVector,
|
||||||
|
500.0f,
|
||||||
|
ECC_Visibility,
|
||||||
|
EDirectiveUtilSurfacePlacement::Bounds,
|
||||||
|
false);
|
||||||
|
TestEqual(TEXT("Bounds snapping changes the actor"), BoundsSnapResult.ChangedActors.Num(), 1);
|
||||||
|
TestTrue(TEXT("Bounds snapping places the lower bound on the surface"), FMath::IsNearlyEqual(BoundsActor->GetActorLocation().Z, 20.0f, 0.1f));
|
||||||
|
|
||||||
|
AActor* NormalActor = SpawnBox(FVector(-150.0f, -150.0f, 100.0f), FVector(10.0f), ECC_WorldDynamic);
|
||||||
|
NormalActor->SetActorRotation(FRotator(45.0f, 0.0f, 0.0f));
|
||||||
|
FDirectiveUtilActorOperationResult NormalSnapResult = UDirectiveUtilEditorActorSubsystem::SnapActorsToSurface(
|
||||||
|
{NormalActor},
|
||||||
|
FVector::DownVector,
|
||||||
|
500.0f,
|
||||||
|
ECC_Visibility,
|
||||||
|
EDirectiveUtilSurfacePlacement::Pivot,
|
||||||
|
true);
|
||||||
|
TestEqual(TEXT("Normal-aligned snapping changes the actor"), NormalSnapResult.ChangedActors.Num(), 1);
|
||||||
|
TestTrue(TEXT("Normal-aligned snapping points the actor up from the surface"), NormalActor->GetActorUpVector().Equals(FVector::UpVector, 0.01f));
|
||||||
|
|
||||||
|
AActor* MissActor = SpawnBox(FVector(500.0f, 500.0f, 100.0f), FVector(10.0f), ECC_WorldDynamic);
|
||||||
|
FDirectiveUtilActorOperationResult MissResult = UDirectiveUtilEditorActorSubsystem::SnapActorsToSurface(
|
||||||
|
{MissActor},
|
||||||
|
FVector::DownVector,
|
||||||
|
500.0f,
|
||||||
|
ECC_Visibility,
|
||||||
|
EDirectiveUtilSurfacePlacement::Pivot,
|
||||||
|
false);
|
||||||
|
TestTrue(TEXT("Surface snapping reports collision misses as skipped"), MissResult.SkippedActors.Contains(MissActor));
|
||||||
|
|
||||||
|
Floor->Destroy();
|
||||||
|
AActor* Ceiling = SpawnBox(FVector(0.0f, 0.0f, 50.0f), FVector(20.0f, 20.0f, 10.0f), ECC_WorldStatic);
|
||||||
|
World->UpdateWorldComponents(true, false);
|
||||||
|
AActor* ActorWithoutRoot = World->SpawnActor<AActor>();
|
||||||
|
ActorWithoutRoot->ClearFlags(RF_Transient);
|
||||||
|
FDirectiveUtilActorOperationResult FailedMoveResult = UDirectiveUtilEditorActorSubsystem::SnapActorsToSurface(
|
||||||
|
{ActorWithoutRoot},
|
||||||
|
FVector::UpVector,
|
||||||
|
500.0f,
|
||||||
|
ECC_Visibility,
|
||||||
|
EDirectiveUtilSurfacePlacement::Pivot,
|
||||||
|
false);
|
||||||
|
TestTrue(TEXT("Surface snapping reports a failed transform as skipped"), FailedMoveResult.SkippedActors.Contains(ActorWithoutRoot));
|
||||||
|
TestFalse(TEXT("A failed transform is not reported as changed"), FailedMoveResult.ChangedActors.Contains(ActorWithoutRoot));
|
||||||
|
|
||||||
|
AActor* TransientActor = SpawnBox(FVector::ZeroVector, FVector(10.0f), ECC_WorldDynamic);
|
||||||
|
TransientActor->SetFlags(RF_Transient);
|
||||||
|
FDirectiveUtilActorOperationResult InvalidResult = UDirectiveUtilEditorActorSubsystem::AlignActors(
|
||||||
|
{First, TransientActor, nullptr},
|
||||||
|
EDirectiveUtilActorLayoutAxis::X,
|
||||||
|
EDirectiveUtilActorAlignment::Center);
|
||||||
|
TestTrue(TEXT("Transient actors are skipped"), InvalidResult.SkippedActors.Contains(TransientActor));
|
||||||
|
|
||||||
|
Ceiling->Destroy();
|
||||||
|
GEngine->DestroyWorldContext(World);
|
||||||
|
World->DestroyWorld(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
#if WITH_EDITOR
|
#if WITH_EDITOR
|
||||||
|
|
||||||
#include "Subsystems/DirectiveUtilEditorActorSubsystem.h"
|
#include "Subsystems/DirectiveUtilEditorActorSubsystem.h"
|
||||||
@@ -5,17 +7,23 @@
|
|||||||
#include "Misc/AutomationTest.h"
|
#include "Misc/AutomationTest.h"
|
||||||
#include "Engine/StaticMesh.h"
|
#include "Engine/StaticMesh.h"
|
||||||
#include "Engine/StaticMeshActor.h"
|
#include "Engine/StaticMeshActor.h"
|
||||||
|
#include "Engine/Texture2D.h"
|
||||||
#include "Components/StaticMeshComponent.h"
|
#include "Components/StaticMeshComponent.h"
|
||||||
#include "Components/BoxComponent.h"
|
#include "Components/BoxComponent.h"
|
||||||
#include "Components/CapsuleComponent.h"
|
#include "Components/CapsuleComponent.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "Materials/MaterialExpressionTextureSample.h"
|
||||||
#include "Materials/MaterialInterface.h"
|
#include "Materials/MaterialInterface.h"
|
||||||
#include "Engine/World.h"
|
#include "Engine/World.h"
|
||||||
#include "Engine/Engine.h"
|
#include "Engine/Engine.h"
|
||||||
|
#include "UObject/Package.h"
|
||||||
|
|
||||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemTest, "DirectiveUtilities.EditorActorSubsystemTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemTest, "DirectiveUtilities.EditorActorSubsystemTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
bool FDirectiveUtilEditorActorSubsystemTest::RunTest(const FString& Parameters)
|
bool FDirectiveUtilEditorActorSubsystemTest::RunTest(const FString& Parameters)
|
||||||
{
|
{
|
||||||
|
UDirectiveUtilEditorActorSubsystem::FocusActorsInViewport({nullptr});
|
||||||
|
|
||||||
// IsActorWithinBoxBounds should not crash and should return false with null Actor
|
// IsActorWithinBoxBounds should not crash and should return false with null Actor
|
||||||
TestFalse("IsActorWithinBoxBounds should return false with null Actor",
|
TestFalse("IsActorWithinBoxBounds should return false with null Actor",
|
||||||
UDirectiveUtilEditorActorSubsystem::IsActorWithinBoxBounds(nullptr, nullptr));
|
UDirectiveUtilEditorActorSubsystem::IsActorWithinBoxBounds(nullptr, nullptr));
|
||||||
@@ -60,8 +68,8 @@ bool FDirectiveUtilEditorActorSubsystemFilterTest::RunTest(const FString& Parame
|
|||||||
UStaticMesh* Sphere = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Sphere.Sphere"));
|
UStaticMesh* Sphere = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Sphere.Sphere"));
|
||||||
if (!Cube || !Sphere)
|
if (!Cube || !Sphere)
|
||||||
{
|
{
|
||||||
AddInfo(TEXT("Engine basic shapes unavailable; skipping include/exclude behaviour test."));
|
AddError(TEXT("Engine basic shapes unavailable for the include/exclude behaviour test."));
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
|
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
|
||||||
@@ -126,8 +134,8 @@ bool FDirectiveUtilEditorActorSubsystemFilterCoverageTest::RunTest(const FString
|
|||||||
UMaterialInterface* MatB = LoadObject<UMaterialInterface>(nullptr, TEXT("/Engine/EngineMaterials/WorldGridMaterial.WorldGridMaterial"));
|
UMaterialInterface* MatB = LoadObject<UMaterialInterface>(nullptr, TEXT("/Engine/EngineMaterials/WorldGridMaterial.WorldGridMaterial"));
|
||||||
if (!Cube || !Sphere || !MatA || !MatB)
|
if (!Cube || !Sphere || !MatA || !MatB)
|
||||||
{
|
{
|
||||||
AddInfo(TEXT("Engine basic shapes/materials unavailable; skipping filter coverage test."));
|
AddError(TEXT("Engine basic shapes/materials unavailable for the filter coverage test."));
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
|
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
|
||||||
@@ -257,6 +265,31 @@ bool FDirectiveUtilEditorActorSubsystemFilterCoverageTest::RunTest(const FString
|
|||||||
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));
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Static-mesh bounds use the source mesh dimensions, independently of actor transform.
|
||||||
|
{
|
||||||
|
const FVector MeshSize = Cube->GetBounds().BoxExtent * 2.0f;
|
||||||
|
const TArray<AActor*> Match = RunFilter([&](const TArray<AActor*>& S, TArray<AActor*>& O)
|
||||||
|
{
|
||||||
|
UDirectiveUtilEditorActorSubsystem::FilterActorsByStaticMeshBounds(
|
||||||
|
S, O, MeshSize - FVector(1.0f), MeshSize + FVector(1.0f), Include);
|
||||||
|
});
|
||||||
|
TestTrue("ByStaticMeshBounds includes the cube actor", Match.Contains(ActorA));
|
||||||
|
TestFalse("ByStaticMeshBounds excludes the actor without a mesh", Match.Contains(ActorC));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override lightmap resolution is a distinct search lane from the mesh default.
|
||||||
|
{
|
||||||
|
CompA->bOverrideLightMapRes = true;
|
||||||
|
CompA->OverriddenLightMapRes = 128;
|
||||||
|
const TArray<AActor*> Match = RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O)
|
||||||
|
{
|
||||||
|
UDirectiveUtilEditorActorSubsystem::FilterActorsByLightmapResolution(
|
||||||
|
S, O, 128, 128, OverrideOnly, Include);
|
||||||
|
});
|
||||||
|
TestTrue("ByLightmapResolution finds the exact override", Match.Contains(ActorA));
|
||||||
|
TestFalse("ByLightmapResolution rejects a different override", Match.Contains(ActorB));
|
||||||
|
}
|
||||||
|
|
||||||
// World location: A at origin, B far away.
|
// 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); });
|
const TArray<AActor*> Near = RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O){ UDirectiveUtilEditorActorSubsystem::FilterActorsByWorldLocation(S, O, FVector::ZeroVector, 50.0f, Include); });
|
||||||
@@ -269,6 +302,67 @@ bool FDirectiveUtilEditorActorSubsystemFilterCoverageTest::RunTest(const FString
|
|||||||
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);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Texture-reference filtering must follow the same include/exclude contract.
|
||||||
|
{
|
||||||
|
UTexture2D* AbsentTexture = NewObject<UTexture2D>(GetTransientPackage());
|
||||||
|
const TArray<AActor*> Inc = RunFilter([&](const TArray<AActor*>& S, TArray<AActor*>& O)
|
||||||
|
{
|
||||||
|
UDirectiveUtilEditorActorSubsystem::FilterActorsByTexture(S, O, AbsentTexture, BaseAndOverride, Include);
|
||||||
|
});
|
||||||
|
const TArray<AActor*> Exc = RunFilter([&](const TArray<AActor*>& S, TArray<AActor*>& O)
|
||||||
|
{
|
||||||
|
UDirectiveUtilEditorActorSubsystem::FilterActorsByTexture(S, O, AbsentTexture, BaseAndOverride, Exclude);
|
||||||
|
});
|
||||||
|
TestEqual("ByTexture(absent) Include returns none", Inc.Num(), 0);
|
||||||
|
TestEqual("ByTexture(absent) Exclude returns every valid source actor", Exc.Num(), Src.Num());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Missing-resource convenience filters are semantic aliases, not merely spawn-tested nodes.
|
||||||
|
{
|
||||||
|
UStaticMesh* MissingMaterialMesh = DuplicateObject<UStaticMesh>(Cube, GetTransientPackage());
|
||||||
|
MissingMaterialMesh->SetMaterial(0, nullptr);
|
||||||
|
UStaticMeshComponent* MissingMaterialComp = SpawnMesh(MissingMaterialMesh, EComponentMobility::Static);
|
||||||
|
UStaticMeshComponent* MissingMeshComp = SpawnMesh(nullptr, EComponentMobility::Static);
|
||||||
|
const TArray<AActor*> MissingSource = { MissingMaterialComp->GetOwner(), MissingMeshComp->GetOwner(), ActorA };
|
||||||
|
TArray<AActor*> MissingMaterials;
|
||||||
|
UDirectiveUtilEditorActorSubsystem::FilterActorsByMissingMaterials(
|
||||||
|
MissingSource, MissingMaterials, BaseOnly, Include);
|
||||||
|
TestTrue("MissingMaterials finds the null source material", MissingMaterials.Contains(MissingMaterialComp->GetOwner()));
|
||||||
|
TestFalse("MissingMaterials rejects the valid cube material", MissingMaterials.Contains(ActorA));
|
||||||
|
TArray<AActor*> MissingMeshes;
|
||||||
|
UDirectiveUtilEditorActorSubsystem::FilterActorsByMissingStaticMeshes(MissingSource, MissingMeshes, Include);
|
||||||
|
TestTrue("MissingStaticMeshes finds a mesh component with no mesh", MissingMeshes.Contains(MissingMeshComp->GetOwner()));
|
||||||
|
TestFalse("MissingStaticMeshes rejects a valid mesh", MissingMeshes.Contains(ActorA));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
UMaterial* MissingTextureMaterial = NewObject<UMaterial>(GetTransientPackage());
|
||||||
|
UMaterialExpressionTextureSample* MissingTextureExpression = NewObject<UMaterialExpressionTextureSample>(MissingTextureMaterial);
|
||||||
|
MissingTextureMaterial->GetExpressionCollection().AddExpression(MissingTextureExpression);
|
||||||
|
CompA->SetMaterial(0, MissingTextureMaterial);
|
||||||
|
const TArray<AActor*> MissingTextures = RunFilter([](const TArray<AActor*>& S, TArray<AActor*>& O)
|
||||||
|
{
|
||||||
|
UDirectiveUtilEditorActorSubsystem::FilterActorsByMissingTextures(S, O, OverrideOnly, Include);
|
||||||
|
});
|
||||||
|
TestTrue("Missing texture filter should include an actor with an unset texture expression", MissingTextures.Contains(ActorA));
|
||||||
|
TestFalse("Missing texture filter should exclude actors without unset texture expressions", MissingTextures.Contains(ActorB));
|
||||||
|
CompA->SetMaterial(0, MatA);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
AActor* DestroyedActor = World->SpawnActor<AActor>();
|
||||||
|
World->DestroyActor(DestroyedActor);
|
||||||
|
TestFalse("Destroyed actor should be invalid", IsValid(DestroyedActor));
|
||||||
|
TArray<AActor*> InvalidActors = {DestroyedActor};
|
||||||
|
TArray<AActor*> FilteredInvalidActors;
|
||||||
|
UDirectiveUtilEditorActorSubsystem::FilterActorsByTag(
|
||||||
|
InvalidActors,
|
||||||
|
FilteredInvalidActors,
|
||||||
|
TEXT("Absent"),
|
||||||
|
Exclude);
|
||||||
|
TestEqual("Actor filters should skip invalid actors", FilteredInvalidActors.Num(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
GEngine->DestroyWorldContext(World);
|
GEngine->DestroyWorldContext(World);
|
||||||
World->DestroyWorld(false);
|
World->DestroyWorld(false);
|
||||||
|
|
||||||
@@ -327,10 +421,12 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemQueryAlignmen
|
|||||||
bool FDirectiveUtilEditorActorSubsystemQueryAlignmentTest::RunTest(const FString& Parameters)
|
bool FDirectiveUtilEditorActorSubsystemQueryAlignmentTest::RunTest(const FString& Parameters)
|
||||||
{
|
{
|
||||||
UStaticMesh* Cube = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
|
UStaticMesh* Cube = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
|
||||||
if (!Cube)
|
UMaterialInterface* BasicMaterial = LoadObject<UMaterialInterface>(
|
||||||
|
nullptr, TEXT("/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial"));
|
||||||
|
if (!Cube || !BasicMaterial)
|
||||||
{
|
{
|
||||||
AddInfo(TEXT("Engine basic shapes unavailable; skipping query alignment test."));
|
AddError(TEXT("Engine basic shapes or materials unavailable for the query alignment test."));
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The GetActorsBy* queries read from the editor world rather than a passed array.
|
// The GetActorsBy* queries read from the editor world rather than a passed array.
|
||||||
@@ -345,8 +441,8 @@ bool FDirectiveUtilEditorActorSubsystemQueryAlignmentTest::RunTest(const FString
|
|||||||
}
|
}
|
||||||
if (!EditorWorld)
|
if (!EditorWorld)
|
||||||
{
|
{
|
||||||
AddInfo(TEXT("No editor world available; skipping query alignment test."));
|
AddError(TEXT("No editor world available for the query alignment test."));
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
AActor* MeshActor = EditorWorld->SpawnActor<AActor>();
|
AActor* MeshActor = EditorWorld->SpawnActor<AActor>();
|
||||||
@@ -358,6 +454,7 @@ bool FDirectiveUtilEditorActorSubsystemQueryAlignmentTest::RunTest(const FString
|
|||||||
MeshComponent->SetMobility(EComponentMobility::Movable);
|
MeshComponent->SetMobility(EComponentMobility::Movable);
|
||||||
MeshComponent->SetupAttachment(Root);
|
MeshComponent->SetupAttachment(Root);
|
||||||
MeshComponent->SetStaticMesh(Cube);
|
MeshComponent->SetStaticMesh(Cube);
|
||||||
|
MeshComponent->SetMaterial(0, BasicMaterial);
|
||||||
MeshComponent->RegisterComponent();
|
MeshComponent->RegisterComponent();
|
||||||
MeshActor->AddInstanceComponent(MeshComponent);
|
MeshActor->AddInstanceComponent(MeshComponent);
|
||||||
MeshActor->SetActorLocation(FVector::ZeroVector);
|
MeshActor->SetActorLocation(FVector::ZeroVector);
|
||||||
@@ -365,6 +462,10 @@ bool FDirectiveUtilEditorActorSubsystemQueryAlignmentTest::RunTest(const FString
|
|||||||
// The query methods keep no instance state, so a transient instance is enough headless.
|
// The query methods keep no instance state, so a transient instance is enough headless.
|
||||||
UDirectiveUtilEditorActorSubsystem* Subsystem = NewObject<UDirectiveUtilEditorActorSubsystem>();
|
UDirectiveUtilEditorActorSubsystem* Subsystem = NewObject<UDirectiveUtilEditorActorSubsystem>();
|
||||||
|
|
||||||
|
TArray<AActor*> ByClass;
|
||||||
|
Subsystem->GetActorsByClass(ByClass, AActor::StaticClass(), World, Include);
|
||||||
|
TestTrue("Class: AActor query finds the fixture actor", ByClass.Contains(MeshActor));
|
||||||
|
|
||||||
// Bounding box: an enclosing box finds the actor, a disjoint one does not.
|
// Bounding box: an enclosing box finds the actor, a disjoint one does not.
|
||||||
TArray<AActor*> InBox;
|
TArray<AActor*> InBox;
|
||||||
Subsystem->GetActorsByBoundingBox(InBox, FVector(-100000.0f), FVector(100000.0f), World, Include);
|
Subsystem->GetActorsByBoundingBox(InBox, FVector(-100000.0f), FVector(100000.0f), World, Include);
|
||||||
@@ -378,6 +479,81 @@ bool FDirectiveUtilEditorActorSubsystemQueryAlignmentTest::RunTest(const FString
|
|||||||
Subsystem->GetActorsByStaticMeshName(ByName, TEXT("cub"), World, Include);
|
Subsystem->GetActorsByStaticMeshName(ByName, TEXT("cub"), World, Include);
|
||||||
TestTrue("StaticMeshName: lowercase substring finds the actor", ByName.Contains(MeshActor));
|
TestTrue("StaticMeshName: lowercase substring finds the actor", ByName.Contains(MeshActor));
|
||||||
|
|
||||||
|
TArray<AActor*> ByMaterial;
|
||||||
|
Subsystem->GetActorsByMaterial(ByMaterial, BasicMaterial, OverrideOnly, World, Include);
|
||||||
|
TestTrue("Material: generic actor with a static mesh component is included", ByMaterial.Contains(MeshActor));
|
||||||
|
TArray<AActor*> ByMaterialSoftReference;
|
||||||
|
Subsystem->GetActorsByMaterialSoftReference(
|
||||||
|
ByMaterialSoftReference, BasicMaterial, OverrideOnly, World, Include);
|
||||||
|
TestTrue("Material soft reference: generic actor with a static mesh component is included",
|
||||||
|
ByMaterialSoftReference.Contains(MeshActor));
|
||||||
|
TArray<AActor*> ByMaterialName;
|
||||||
|
Subsystem->GetActorsByMaterialName(
|
||||||
|
ByMaterialName, BasicMaterial->GetName(), OverrideOnly, World, Include);
|
||||||
|
TestTrue("Material name: generic actor with a static mesh component is included",
|
||||||
|
ByMaterialName.Contains(MeshActor));
|
||||||
|
TArray<AActor*> WithoutMaterial;
|
||||||
|
Subsystem->GetActorsByMaterial(WithoutMaterial, BasicMaterial, OverrideOnly, World, Exclude);
|
||||||
|
TestFalse("Material exclude: matching generic actor is excluded", WithoutMaterial.Contains(MeshActor));
|
||||||
|
|
||||||
|
const int32 VertexCount = Cube->GetNumVertices(0);
|
||||||
|
TArray<AActor*> ByVertexCount;
|
||||||
|
Subsystem->GetActorsByVertexCount(ByVertexCount, VertexCount, VertexCount, World, Include);
|
||||||
|
TestTrue("VertexCount: exact range finds the fixture actor", ByVertexCount.Contains(MeshActor));
|
||||||
|
const int32 TriangleCount = Cube->GetNumTriangles(0);
|
||||||
|
TArray<AActor*> ByTriangleCount;
|
||||||
|
Subsystem->GetActorsByTriCount(ByTriangleCount, TriangleCount, TriangleCount, World, Include);
|
||||||
|
TestTrue("TriangleCount: exact range finds the fixture actor", ByTriangleCount.Contains(MeshActor));
|
||||||
|
|
||||||
|
const float MeshSize = Cube->GetBoundingBox().GetSize().Size();
|
||||||
|
TArray<AActor*> ByMeshSize;
|
||||||
|
Subsystem->GetActorsByMeshSize(ByMeshSize, MeshSize - 1.0f, MeshSize + 1.0f, World, Include);
|
||||||
|
TestTrue("MeshSize: enclosing range finds the fixture actor", ByMeshSize.Contains(MeshActor));
|
||||||
|
TArray<AActor*> ByWorldLocation;
|
||||||
|
Subsystem->GetActorsByWorldLocation(ByWorldLocation, FVector::ZeroVector, 100.0f, World, Include);
|
||||||
|
TestTrue("WorldLocation: nearby query finds the fixture actor", ByWorldLocation.Contains(MeshActor));
|
||||||
|
|
||||||
|
const int32 LODCount = Cube->GetNumLODs();
|
||||||
|
TArray<AActor*> ByLODCount;
|
||||||
|
Subsystem->GetActorsByLODCount(ByLODCount, LODCount, LODCount, World, Include);
|
||||||
|
TestTrue("LODCount: exact range finds the fixture actor", ByLODCount.Contains(MeshActor));
|
||||||
|
TArray<AActor*> NaniteOff;
|
||||||
|
TArray<AActor*> NaniteOn;
|
||||||
|
Subsystem->GetActorsByNaniteEnabled(NaniteOff, false, World, Include);
|
||||||
|
Subsystem->GetActorsByNaniteEnabled(NaniteOn, true, World, Include);
|
||||||
|
TestTrue("Nanite: fixture belongs to exactly one state", NaniteOff.Contains(MeshActor) != NaniteOn.Contains(MeshActor));
|
||||||
|
|
||||||
|
const int32 SourceLightmapResolution = Cube->GetLightMapResolution();
|
||||||
|
TArray<AActor*> ByLightmapResolution;
|
||||||
|
Subsystem->GetActorsByLightmapResolution(
|
||||||
|
ByLightmapResolution, SourceLightmapResolution, SourceLightmapResolution, World, Include);
|
||||||
|
TestTrue("LightmapResolution: exact source resolution finds the fixture actor", ByLightmapResolution.Contains(MeshActor));
|
||||||
|
TArray<AActor*> ByMobility;
|
||||||
|
Subsystem->GetActorsByMobility(ByMobility, EComponentMobility::Movable, World, Include);
|
||||||
|
TestTrue("Mobility query finds the movable fixture actor", ByMobility.Contains(MeshActor));
|
||||||
|
|
||||||
|
TArray<AActor*> ByStaticMesh;
|
||||||
|
Subsystem->GetActorsByStaticMesh(ByStaticMesh, Cube, World, Include);
|
||||||
|
TestTrue("StaticMesh reference query finds the fixture actor", ByStaticMesh.Contains(MeshActor));
|
||||||
|
TArray<AActor*> ByStaticMeshSoft;
|
||||||
|
Subsystem->GetActorsByStaticMeshSoftReference(ByStaticMeshSoft, Cube, World, Include);
|
||||||
|
TestTrue("StaticMesh soft-reference query finds the fixture actor", ByStaticMeshSoft.Contains(MeshActor));
|
||||||
|
|
||||||
|
UTexture2D* AbsentTexture = NewObject<UTexture2D>(GetTransientPackage());
|
||||||
|
TArray<AActor*> ByTexture;
|
||||||
|
Subsystem->GetActorsByTexture(ByTexture, AbsentTexture, World, Include);
|
||||||
|
TestFalse("Texture reference query rejects an absent texture", ByTexture.Contains(MeshActor));
|
||||||
|
TArray<AActor*> ByTextureSoft;
|
||||||
|
Subsystem->GetActorsByTextureSoftReference(ByTextureSoft, AbsentTexture, World, Include);
|
||||||
|
TestFalse("Texture soft-reference query rejects an absent texture", ByTextureSoft.Contains(MeshActor));
|
||||||
|
TArray<AActor*> ByTextureName;
|
||||||
|
Subsystem->GetActorsByTextureName(ByTextureName, TEXT("__udcore_absent_texture__"), World, Include);
|
||||||
|
TestFalse("Texture-name query rejects an absent texture", ByTextureName.Contains(MeshActor));
|
||||||
|
|
||||||
|
TArray<AActor*> InvalidActors = { MeshActor };
|
||||||
|
Subsystem->GetInvalidActors(InvalidActors);
|
||||||
|
TestEqual("GetInvalidActors resets its deprecated output", InvalidActors.Num(), 0);
|
||||||
|
|
||||||
// Mobility: the root component's mobility is what counts.
|
// Mobility: the root component's mobility is what counts.
|
||||||
const TArray<AActor*> Source = { MeshActor };
|
const TArray<AActor*> Source = { MeshActor };
|
||||||
TArray<AActor*> MovableActors;
|
TArray<AActor*> MovableActors;
|
||||||
@@ -392,6 +568,47 @@ bool FDirectiveUtilEditorActorSubsystemQueryAlignmentTest::RunTest(const FString
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilPushOverrideMaterialsTest, "DirectiveUtilities.PushOverrideMaterialsTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FDirectiveUtilPushOverrideMaterialsTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
UMaterialInterface* BaseMaterial = LoadObject<UMaterialInterface>(
|
||||||
|
nullptr, TEXT("/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial"));
|
||||||
|
UMaterialInterface* OverrideMaterial = LoadObject<UMaterialInterface>(
|
||||||
|
nullptr, TEXT("/Engine/EngineMaterials/WorldGridMaterial.WorldGridMaterial"));
|
||||||
|
UStaticMesh* Cube = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
|
||||||
|
if (!BaseMaterial || !OverrideMaterial || !Cube)
|
||||||
|
{
|
||||||
|
AddError(TEXT("Engine materials unavailable for the override material test."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UStaticMesh* StaticMesh = DuplicateObject<UStaticMesh>(Cube, GetTransientPackage());
|
||||||
|
StaticMesh->GetStaticMaterials()[0].MaterialInterface = BaseMaterial;
|
||||||
|
UStaticMeshComponent* StaticMeshComponent = NewObject<UStaticMeshComponent>(GetTransientPackage());
|
||||||
|
StaticMeshComponent->SetStaticMesh(StaticMesh);
|
||||||
|
StaticMeshComponent->SetMaterial(0, OverrideMaterial);
|
||||||
|
|
||||||
|
UDirectiveUtilEditorActorSubsystem::PushOverrideMaterialsToSource(StaticMeshComponent);
|
||||||
|
TestEqual("Override material is copied to its source slot", StaticMesh->GetMaterial(0), OverrideMaterial);
|
||||||
|
|
||||||
|
UStaticMesh* MeshWithoutOverride = DuplicateObject<UStaticMesh>(Cube, GetTransientPackage());
|
||||||
|
MeshWithoutOverride->GetStaticMaterials()[0].MaterialInterface = BaseMaterial;
|
||||||
|
UStaticMeshComponent* ComponentWithoutOverride = NewObject<UStaticMeshComponent>(GetTransientPackage());
|
||||||
|
ComponentWithoutOverride->SetStaticMesh(MeshWithoutOverride);
|
||||||
|
UDirectiveUtilEditorActorSubsystem::PushOverrideMaterialsToSource(ComponentWithoutOverride);
|
||||||
|
TestEqual("A source slot is unchanged when the component has no override",
|
||||||
|
MeshWithoutOverride->GetMaterial(0), BaseMaterial);
|
||||||
|
|
||||||
|
AddExpectedError(TEXT("Static Mesh Component is invalid."), EAutomationExpectedErrorFlags::Exact, 1);
|
||||||
|
UDirectiveUtilEditorActorSubsystem::PushOverrideMaterialsToSource(nullptr);
|
||||||
|
UStaticMeshComponent* ComponentWithoutMesh = NewObject<UStaticMeshComponent>(GetTransientPackage());
|
||||||
|
AddExpectedError(TEXT("Static Mesh Component has no valid static mesh."), EAutomationExpectedErrorFlags::Exact, 1);
|
||||||
|
UDirectiveUtilEditorActorSubsystem::PushOverrideMaterialsToSource(ComponentWithoutMesh);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemBoundsTest, "DirectiveUtilities.EditorActorSubsystemBoundsTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemBoundsTest, "DirectiveUtilities.EditorActorSubsystemBoundsTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
bool FDirectiveUtilEditorActorSubsystemBoundsTest::RunTest(const FString& Parameters)
|
bool FDirectiveUtilEditorActorSubsystemBoundsTest::RunTest(const FString& Parameters)
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "Libraries/DirectiveUtilEditorAssetAuditLibrary.h"
|
||||||
|
#include "Misc/AutomationTest.h"
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
|
FDirectiveUtilEditorAssetAuditLibraryTest,
|
||||||
|
"DirectiveUtilities.EditorAssetAuditLibraryTests",
|
||||||
|
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FDirectiveUtilEditorAssetAuditLibraryTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
FDirectiveUtilAssetAuditOptions Options;
|
||||||
|
Options.PackagePaths = {TEXT("/Engine/BasicShapes")};
|
||||||
|
|
||||||
|
const FDirectiveUtilAssetAuditReport Report = UDirectiveUtilEditorAssetAuditLibrary::BuildAssetAuditReport(Options);
|
||||||
|
TestTrue(TEXT("The engine shape scan returns assets"), !Report.Assets.IsEmpty());
|
||||||
|
if (const IAssetRegistry* AssetRegistry = IAssetRegistry::Get())
|
||||||
|
{
|
||||||
|
TestTrue(TEXT("An asset audit starts the initial registry scan"), AssetRegistry->IsSearchAllAssets());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const FDirectiveUtilAssetAuditEntry& Entry : Report.Assets)
|
||||||
|
{
|
||||||
|
TestTrue(TEXT("Report assets have valid data"), Entry.Asset.IsValid());
|
||||||
|
TestTrue(TEXT("Report assets have a package"), !Entry.PackageName.IsNone());
|
||||||
|
TestTrue(TEXT("Report assets have a class"), !Entry.AssetClass.IsEmpty());
|
||||||
|
TestTrue(TEXT("Report counts are non-negative"), Entry.DependencyCount >= 0 && Entry.ReferencerCount >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FString Csv = UDirectiveUtilEditorAssetAuditLibrary::AssetAuditReportToCsv(Report);
|
||||||
|
TestTrue(TEXT("CSV includes its header"), Csv.StartsWith(TEXT("Asset,Package,Path,Class")));
|
||||||
|
TestTrue(TEXT("CSV includes scanned assets"), Csv.Contains(TEXT("/Engine/BasicShapes")));
|
||||||
|
|
||||||
|
Options.ExcludedPackagePaths = {TEXT("/Engine/BasicShapes")};
|
||||||
|
TestTrue(
|
||||||
|
TEXT("Excluded paths return no candidates"),
|
||||||
|
UDirectiveUtilEditorAssetAuditLibrary::FindUnreferencedAssetCandidates(Options).IsEmpty());
|
||||||
|
TestTrue(
|
||||||
|
TEXT("Excluded paths return no missing references"),
|
||||||
|
UDirectiveUtilEditorAssetAuditLibrary::FindMissingAssetReferences(Options).IsEmpty());
|
||||||
|
TestTrue(
|
||||||
|
TEXT("Excluded paths return no cycles"),
|
||||||
|
UDirectiveUtilEditorAssetAuditLibrary::FindAssetDependencyCycles(Options).IsEmpty());
|
||||||
|
|
||||||
|
FDirectiveUtilAssetAuditOptions EngineOptions;
|
||||||
|
EngineOptions.PackagePaths = {TEXT("/Engine")};
|
||||||
|
const FDirectiveUtilAssetAuditReport EngineReport = UDirectiveUtilEditorAssetAuditLibrary::BuildAssetAuditReport(EngineOptions);
|
||||||
|
TestTrue(TEXT("A full engine audit returns more than the basic shapes scan"), EngineReport.Assets.Num() > Report.Assets.Num());
|
||||||
|
for (const FDirectiveUtilAssetDependencyCycle& Cycle : EngineReport.DependencyCycles)
|
||||||
|
{
|
||||||
|
TestTrue(TEXT("Dependency cycles contain at least one package"), !Cycle.Packages.IsEmpty());
|
||||||
|
for (int32 Index = 1; Index < Cycle.Packages.Num(); ++Index)
|
||||||
|
{
|
||||||
|
TestTrue(TEXT("Dependency cycle packages are sorted"), Cycle.Packages[Index - 1].LexicalLess(Cycle.Packages[Index]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -1,8 +1,18 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
#if WITH_EDITOR
|
#if WITH_EDITOR
|
||||||
|
|
||||||
|
#include "AssetRegistry/AssetData.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "Editor.h"
|
||||||
#include "Libraries/DirectiveUtilEditorAssetLibrary.h"
|
#include "Libraries/DirectiveUtilEditorAssetLibrary.h"
|
||||||
#include "Engine/World.h"
|
#include "Engine/World.h"
|
||||||
#include "Misc/AutomationTest.h"
|
#include "Misc/AutomationTest.h"
|
||||||
|
#include "Misc/Guid.h"
|
||||||
|
#include "ObjectTools.h"
|
||||||
|
#include "Subsystems/EditorAssetSubsystem.h"
|
||||||
|
#include "Tests/DirectiveUtilTestObject.h"
|
||||||
|
#include "UObject/ObjectRedirector.h"
|
||||||
|
|
||||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorAssetLibraryTest, "DirectiveUtilities.EditorAssetLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorAssetLibraryTest, "DirectiveUtilities.EditorAssetLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
@@ -45,6 +55,78 @@ bool FDirectiveUtilEditorAssetLibraryTest::RunTest(const FString& Parameters)
|
|||||||
TestEqual("FindDuplicateAssets should return each asset path once", UniquePaths.Num(), Pair.Value.DuplicateAssetPaths.Num());
|
TestEqual("FindDuplicateAssets should return each asset path once", UniquePaths.Num(), Pair.Value.DuplicateAssetPaths.Num());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int32 RedirectorsProcessed = INDEX_NONE;
|
||||||
|
TestEqual("FixUpRedirectorsInPaths succeeds when no redirectors match",
|
||||||
|
UDirectiveUtilEditorAssetLibrary::FixUpRedirectorsInPaths(
|
||||||
|
{TEXT("/Game/DirectiveUtilitiesTests/NoRedirectors")}, RedirectorsProcessed),
|
||||||
|
EDirectiveUtilSuccessStatus::Success);
|
||||||
|
TestEqual("No redirectors are reported for an empty path", RedirectorsProcessed, 0);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilFixUpRedirectorsTest, "DirectiveUtilities.FixUpRedirectorsTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FDirectiveUtilFixUpRedirectorsTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
UEditorAssetSubsystem* EditorAssetSubsystem = GEditor
|
||||||
|
? GEditor->GetEditorSubsystem<UEditorAssetSubsystem>()
|
||||||
|
: nullptr;
|
||||||
|
if (!EditorAssetSubsystem)
|
||||||
|
{
|
||||||
|
AddError(TEXT("Editor asset subsystem unavailable for the redirector test."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FString RootPath = FString::Printf(
|
||||||
|
TEXT("/Game/DirectiveUtilitiesTests/Redirectors_%s"),
|
||||||
|
*FGuid::NewGuid().ToString(EGuidFormats::Digits));
|
||||||
|
const FString OriginalName = TEXT("OriginalAsset");
|
||||||
|
const FString OriginalPackageName = RootPath / OriginalName;
|
||||||
|
UPackage* Package = CreatePackage(*OriginalPackageName);
|
||||||
|
UDirectiveUtilTestObject* Asset = NewObject<UDirectiveUtilTestObject>(
|
||||||
|
Package, *OriginalName, RF_Public | RF_Standalone);
|
||||||
|
FAssetRegistryModule::AssetCreated(Asset);
|
||||||
|
|
||||||
|
const FString RenamedAssetName = TEXT("RenamedAsset");
|
||||||
|
ObjectTools::FPackageGroupName PackageGroupName;
|
||||||
|
PackageGroupName.PackageName = RootPath / RenamedAssetName;
|
||||||
|
PackageGroupName.ObjectName = RenamedAssetName;
|
||||||
|
TSet<UPackage*> RefusedPackages;
|
||||||
|
FText RenameError;
|
||||||
|
if (!ObjectTools::RenameSingleObject(
|
||||||
|
Asset, PackageGroupName, RefusedPackages, RenameError, nullptr, true))
|
||||||
|
{
|
||||||
|
EditorAssetSubsystem->DeleteDirectory(RootPath);
|
||||||
|
AddError(FString::Printf(TEXT("Failed to rename the redirector test asset: %s"), *RenameError.ToString()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FString OriginalObjectPath = OriginalPackageName + TEXT(".") + OriginalName;
|
||||||
|
UObjectRedirector* Redirector = FindObject<UObjectRedirector>(nullptr, *OriginalObjectPath);
|
||||||
|
if (!Redirector)
|
||||||
|
{
|
||||||
|
EditorAssetSubsystem->DeleteDirectory(RootPath);
|
||||||
|
AddError(TEXT("Renaming the test asset did not create a redirector."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
TestNotNull("Renaming an asset creates a redirector", Redirector);
|
||||||
|
|
||||||
|
AddExpectedMessagePlain(
|
||||||
|
TEXT("Redirector fix-up requires an interactive editor session."),
|
||||||
|
ELogVerbosity::Warning,
|
||||||
|
EAutomationExpectedMessageFlags::Contains,
|
||||||
|
1);
|
||||||
|
int32 RedirectorsProcessed = INDEX_NONE;
|
||||||
|
const EDirectiveUtilSuccessStatus Status = UDirectiveUtilEditorAssetLibrary::FixUpRedirectorsInPaths(
|
||||||
|
{RootPath}, RedirectorsProcessed);
|
||||||
|
TestEqual("Redirector fix-up fails closed in unattended runs", Status, EDirectiveUtilSuccessStatus::Failure);
|
||||||
|
TestEqual("No redirector is reported as processed after a guarded run", RedirectorsProcessed, 0);
|
||||||
|
TestTrue("Guarded fix-up leaves the redirector intact", EditorAssetSubsystem->DoesAssetExist(OriginalObjectPath));
|
||||||
|
TestTrue("Renamed asset remains available", EditorAssetSubsystem->DoesAssetExist(
|
||||||
|
RootPath / RenamedAssetName + TEXT(".") + RenamedAssetName));
|
||||||
|
|
||||||
|
TestTrue("Redirector test assets are removed", EditorAssetSubsystem->DeleteDirectory(RootPath));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "Components/StaticMeshComponent.h"
|
||||||
|
#include "Libraries/DirectiveUtilEditorBlueprintLibrary.h"
|
||||||
|
#include "Misc/AutomationTest.h"
|
||||||
|
#include "Tests/DirectiveUtilTestObject.h"
|
||||||
|
#include "EdGraphSchema_K2.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Engine/SCS_Node.h"
|
||||||
|
#include "Engine/SimpleConstructionScript.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "Misc/Guid.h"
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
|
FDirectiveUtilEditorBlueprintLibraryTest,
|
||||||
|
"DirectiveUtilities.EditorBlueprintLibraryTests",
|
||||||
|
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FDirectiveUtilEditorBlueprintLibraryTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
auto ContainsBlueprint = [](const TArray<FAssetData>& Assets, const UBlueprint* Expected) {
|
||||||
|
return Assets.ContainsByPredicate([Expected](const FAssetData& Asset) {
|
||||||
|
return Asset.GetAsset() == Expected;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
TestEqual(
|
||||||
|
TEXT("A null Blueprint has unknown status"),
|
||||||
|
UDirectiveUtilEditorBlueprintLibrary::GetBlueprintCompileStatus(nullptr),
|
||||||
|
EDirectiveUtilBlueprintCompileStatus::Unknown);
|
||||||
|
|
||||||
|
UBlueprint* Blueprint = NewObject<UBlueprint>();
|
||||||
|
const TArray<TPair<EBlueprintStatus, EDirectiveUtilBlueprintCompileStatus>> Statuses = {
|
||||||
|
{BS_Unknown, EDirectiveUtilBlueprintCompileStatus::Unknown},
|
||||||
|
{BS_Dirty, EDirectiveUtilBlueprintCompileStatus::Dirty},
|
||||||
|
{BS_Error, EDirectiveUtilBlueprintCompileStatus::Error},
|
||||||
|
{BS_UpToDate, EDirectiveUtilBlueprintCompileStatus::UpToDate},
|
||||||
|
{BS_BeingCreated, EDirectiveUtilBlueprintCompileStatus::BeingCreated},
|
||||||
|
{BS_UpToDateWithWarnings, EDirectiveUtilBlueprintCompileStatus::UpToDateWithWarnings},
|
||||||
|
};
|
||||||
|
for (const TPair<EBlueprintStatus, EDirectiveUtilBlueprintCompileStatus>& Status : Statuses)
|
||||||
|
{
|
||||||
|
Blueprint->Status = Status.Key;
|
||||||
|
TestEqual(
|
||||||
|
TEXT("Blueprint status maps to the public enum"),
|
||||||
|
UDirectiveUtilEditorBlueprintLibrary::GetBlueprintCompileStatus(Blueprint),
|
||||||
|
Status.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
FDirectiveUtilBlueprintSearchOptions Options;
|
||||||
|
Options.PackagePaths = {TEXT("/Engine/BasicShapes")};
|
||||||
|
TestTrue(
|
||||||
|
TEXT("A folder without Blueprints has no compile-status matches"),
|
||||||
|
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsByCompileStatus(
|
||||||
|
EDirectiveUtilBlueprintCompileStatus::UpToDate,
|
||||||
|
Options).IsEmpty());
|
||||||
|
TestTrue(
|
||||||
|
TEXT("A null parent class returns no matches"),
|
||||||
|
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsByParentClass(nullptr, Options).IsEmpty());
|
||||||
|
TestTrue(
|
||||||
|
TEXT("A folder without Blueprints has no parent-class matches"),
|
||||||
|
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsByParentClass(AActor::StaticClass(), Options).IsEmpty());
|
||||||
|
TestTrue(
|
||||||
|
TEXT("A non-interface class returns no interface matches"),
|
||||||
|
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsImplementingInterface(UObject::StaticClass(), Options).IsEmpty());
|
||||||
|
TestTrue(
|
||||||
|
TEXT("A folder without Blueprints has no interface matches"),
|
||||||
|
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsImplementingInterface(UInterface::StaticClass(), Options).IsEmpty());
|
||||||
|
TestTrue(
|
||||||
|
TEXT("A non-component class returns no component matches"),
|
||||||
|
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsContainingComponentClass(UObject::StaticClass(), Options).IsEmpty());
|
||||||
|
TestTrue(
|
||||||
|
TEXT("A folder without Blueprints has no component matches"),
|
||||||
|
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsContainingComponentClass(UActorComponent::StaticClass(), Options).IsEmpty());
|
||||||
|
TestTrue(
|
||||||
|
TEXT("A Blueprint without generated properties has no unused variables"),
|
||||||
|
UDirectiveUtilEditorBlueprintLibrary::GetUnusedBlueprintVariables(Blueprint).IsEmpty());
|
||||||
|
|
||||||
|
const FName FixtureName(*FString::Printf(
|
||||||
|
TEXT("BP_InspectionFixture_%s"),
|
||||||
|
*FGuid::NewGuid().ToString(EGuidFormats::Digits)));
|
||||||
|
const FString FixturePackageName = FString::Printf(
|
||||||
|
TEXT("/Game/DirectiveUtilitiesTests/%s"),
|
||||||
|
*FixtureName.ToString());
|
||||||
|
UPackage* TestPackage = CreatePackage(*FixturePackageName);
|
||||||
|
UBlueprint* InspectionBlueprint = FKismetEditorUtilities::CreateBlueprint(
|
||||||
|
AActor::StaticClass(),
|
||||||
|
TestPackage,
|
||||||
|
FixtureName,
|
||||||
|
BPTYPE_Normal,
|
||||||
|
TEXT("DirectiveUtilities.EditorBlueprintLibraryTests"));
|
||||||
|
if (!TestNotNull(TEXT("The inspection fixture Blueprint is created"), InspectionBlueprint))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestTrue(
|
||||||
|
TEXT("The inspection fixture implements its test interface"),
|
||||||
|
FBlueprintEditorUtils::ImplementNewInterface(
|
||||||
|
InspectionBlueprint,
|
||||||
|
UDirectiveUtilTestInterface::StaticClass()->GetClassPathName()));
|
||||||
|
USCS_Node* ComponentNode = InspectionBlueprint->SimpleConstructionScript->CreateNode(
|
||||||
|
UStaticMeshComponent::StaticClass(),
|
||||||
|
TEXT("InspectionComponent"));
|
||||||
|
InspectionBlueprint->SimpleConstructionScript->AddNode(ComponentNode);
|
||||||
|
FEdGraphPinType VariableType;
|
||||||
|
VariableType.PinCategory = UEdGraphSchema_K2::PC_Int;
|
||||||
|
TestTrue(
|
||||||
|
TEXT("The inspection fixture adds an unused variable"),
|
||||||
|
FBlueprintEditorUtils::AddMemberVariable(InspectionBlueprint, TEXT("UnusedValue"), VariableType));
|
||||||
|
FBlueprintEditorUtils::SetBlueprintVariableMetaData(
|
||||||
|
InspectionBlueprint,
|
||||||
|
TEXT("UnusedValue"),
|
||||||
|
nullptr,
|
||||||
|
FBlueprintMetadata::MD_Private,
|
||||||
|
TEXT("true"));
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(InspectionBlueprint);
|
||||||
|
FKismetEditorUtilities::CompileBlueprint(InspectionBlueprint);
|
||||||
|
FAssetRegistryModule::AssetCreated(InspectionBlueprint);
|
||||||
|
|
||||||
|
FDirectiveUtilBlueprintSearchOptions FixtureOptions;
|
||||||
|
FixtureOptions.PackagePaths = {TEXT("/Game/DirectiveUtilitiesTests")};
|
||||||
|
TestTrue(
|
||||||
|
TEXT("Compile-status search finds the fixture"),
|
||||||
|
ContainsBlueprint(
|
||||||
|
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsByCompileStatus(
|
||||||
|
EDirectiveUtilBlueprintCompileStatus::UpToDate,
|
||||||
|
FixtureOptions),
|
||||||
|
InspectionBlueprint));
|
||||||
|
TestTrue(
|
||||||
|
TEXT("Direct-parent search finds the fixture"),
|
||||||
|
ContainsBlueprint(
|
||||||
|
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsByParentClass(
|
||||||
|
AActor::StaticClass(),
|
||||||
|
FixtureOptions,
|
||||||
|
false),
|
||||||
|
InspectionBlueprint));
|
||||||
|
TestTrue(
|
||||||
|
TEXT("Descendant-parent search finds the fixture"),
|
||||||
|
ContainsBlueprint(
|
||||||
|
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsByParentClass(
|
||||||
|
AActor::StaticClass(),
|
||||||
|
FixtureOptions,
|
||||||
|
true),
|
||||||
|
InspectionBlueprint));
|
||||||
|
TestTrue(
|
||||||
|
TEXT("Interface search finds the fixture"),
|
||||||
|
ContainsBlueprint(
|
||||||
|
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsImplementingInterface(
|
||||||
|
UDirectiveUtilTestInterface::StaticClass(),
|
||||||
|
FixtureOptions),
|
||||||
|
InspectionBlueprint));
|
||||||
|
TestTrue(
|
||||||
|
TEXT("Exact component search finds the fixture"),
|
||||||
|
ContainsBlueprint(
|
||||||
|
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsContainingComponentClass(
|
||||||
|
UStaticMeshComponent::StaticClass(),
|
||||||
|
FixtureOptions,
|
||||||
|
false),
|
||||||
|
InspectionBlueprint));
|
||||||
|
TestTrue(
|
||||||
|
TEXT("Derived component search finds the fixture"),
|
||||||
|
ContainsBlueprint(
|
||||||
|
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsContainingComponentClass(
|
||||||
|
UActorComponent::StaticClass(),
|
||||||
|
FixtureOptions,
|
||||||
|
true),
|
||||||
|
InspectionBlueprint));
|
||||||
|
TestTrue(
|
||||||
|
TEXT("Unused-variable search finds the fixture variable"),
|
||||||
|
UDirectiveUtilEditorBlueprintLibrary::GetUnusedBlueprintVariables(InspectionBlueprint).Contains(TEXT("UnusedValue")));
|
||||||
|
|
||||||
|
FixtureOptions.ExcludedPackagePaths = {TEXT("/Game/DirectiveUtilitiesTests")};
|
||||||
|
TestTrue(
|
||||||
|
TEXT("Excluded paths remove the fixture from search"),
|
||||||
|
UDirectiveUtilEditorBlueprintLibrary::FindBlueprintsByParentClass(
|
||||||
|
AActor::StaticClass(),
|
||||||
|
FixtureOptions).IsEmpty());
|
||||||
|
|
||||||
|
FAssetRegistryModule::AssetDeleted(InspectionBlueprint);
|
||||||
|
TestPackage->SetDirtyFlag(false);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "Libraries/DirectiveUtilEditorTaskLibrary.h"
|
||||||
|
#include "Misc/AutomationTest.h"
|
||||||
|
#include "Tasks/DirectiveUtilEditorSlowTask.h"
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
|
FDirectiveUtilEditorTaskLibraryTest,
|
||||||
|
"DirectiveUtilities.EditorTaskLibraryTests",
|
||||||
|
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FDirectiveUtilEditorTaskLibraryTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
TestNull("StartEditorSlowTask should reject zero work", UDirectiveUtilEditorTaskLibrary::StartEditorSlowTask(0.0f, FText::GetEmpty()));
|
||||||
|
TestNull(
|
||||||
|
"StartEditorSlowTask should reject NaN work",
|
||||||
|
UDirectiveUtilEditorTaskLibrary::StartEditorSlowTask(
|
||||||
|
std::numeric_limits<float>::quiet_NaN(),
|
||||||
|
FText::GetEmpty()));
|
||||||
|
TestNull(
|
||||||
|
"StartEditorSlowTask should reject infinite work",
|
||||||
|
UDirectiveUtilEditorTaskLibrary::StartEditorSlowTask(
|
||||||
|
std::numeric_limits<float>::infinity(),
|
||||||
|
FText::GetEmpty()));
|
||||||
|
UDirectiveUtilEditorSlowTask* PublicTask = UDirectiveUtilEditorTaskLibrary::StartEditorSlowTask(
|
||||||
|
1.0f,
|
||||||
|
FText::FromString(TEXT("Public task")));
|
||||||
|
TestNotNull("StartEditorSlowTask should create a task for positive work", PublicTask);
|
||||||
|
if (PublicTask)
|
||||||
|
{
|
||||||
|
TestNull(
|
||||||
|
"StartEditorSlowTask should reject a nested task",
|
||||||
|
UDirectiveUtilEditorTaskLibrary::StartEditorSlowTask(
|
||||||
|
1.0f,
|
||||||
|
FText::FromString(TEXT("Nested task"))));
|
||||||
|
PublicTask->Finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
UDirectiveUtilEditorSlowTask* Task = NewObject<UDirectiveUtilEditorSlowTask>();
|
||||||
|
Task->Initialize(2.0f, FText::FromString(TEXT("Test task")), false, false);
|
||||||
|
TestTrue("Slow task should be active after initialization", Task->IsActive());
|
||||||
|
TestTrue("Advance should accept non-negative work", Task->Advance(1.0f, FText::FromString(TEXT("Step"))));
|
||||||
|
TestFalse("Advance should reject negative work", Task->Advance(-1.0f, FText::GetEmpty()));
|
||||||
|
TestFalse("Advance should reject NaN work", Task->Advance(std::numeric_limits<float>::quiet_NaN(), FText::GetEmpty()));
|
||||||
|
TestFalse("Advance should reject infinite work", Task->Advance(std::numeric_limits<float>::infinity(), FText::GetEmpty()));
|
||||||
|
TestFalse("A non-cancellable task should not report cancellation", Task->IsCancelRequested());
|
||||||
|
Task->Finish();
|
||||||
|
TestFalse("Slow task should be inactive after Finish", Task->IsActive());
|
||||||
|
TestFalse("Advance should fail after Finish", Task->Advance(1.0f, FText::GetEmpty()));
|
||||||
|
Task->Initialize(std::numeric_limits<float>::quiet_NaN(), FText::GetEmpty(), false, false);
|
||||||
|
TestFalse("Initialize should leave a task inactive for NaN work", Task->IsActive());
|
||||||
|
Task->Initialize(std::numeric_limits<float>::infinity(), FText::GetEmpty(), false, false);
|
||||||
|
TestFalse("Initialize should leave a task inactive for infinite work", Task->IsActive());
|
||||||
|
|
||||||
|
UDirectiveUtilEditorSlowTask* FirstTask = NewObject<UDirectiveUtilEditorSlowTask>();
|
||||||
|
UDirectiveUtilEditorSlowTask* SecondTask = NewObject<UDirectiveUtilEditorSlowTask>();
|
||||||
|
FirstTask->Initialize(1.0f, FText::FromString(TEXT("First task")), false, false);
|
||||||
|
SecondTask->Initialize(1.0f, FText::FromString(TEXT("Second task")), false, false);
|
||||||
|
TestTrue("The first slow task should remain active", FirstTask->IsActive());
|
||||||
|
TestFalse("A concurrent slow task should remain inactive", SecondTask->IsActive());
|
||||||
|
FirstTask->Finish();
|
||||||
|
SecondTask->Initialize(1.0f, FText::FromString(TEXT("Second task")), false, false);
|
||||||
|
TestTrue("A slow task should start after the active task finishes", SecondTask->IsActive());
|
||||||
|
SecondTask->Finish();
|
||||||
|
|
||||||
|
TWeakObjectPtr<UDirectiveUtilEditorSlowTask> AbandonedTask = NewObject<UDirectiveUtilEditorSlowTask>();
|
||||||
|
AbandonedTask->Initialize(1.0f, FText::FromString(TEXT("Abandoned task")), false, false);
|
||||||
|
TestTrue("An unfinished slow task should be active before collection", AbandonedTask->IsActive());
|
||||||
|
CollectGarbage(RF_NoFlags);
|
||||||
|
TestTrue("An unfinished slow task should stay alive until Finish", AbandonedTask.IsValid());
|
||||||
|
if (AbandonedTask.IsValid())
|
||||||
|
{
|
||||||
|
AbandonedTask->Finish();
|
||||||
|
}
|
||||||
|
CollectGarbage(RF_NoFlags);
|
||||||
|
TestFalse("A finished slow task should be collectable", AbandonedTask.IsValid());
|
||||||
|
UDirectiveUtilEditorSlowTask* TaskAfterCollection = NewObject<UDirectiveUtilEditorSlowTask>();
|
||||||
|
TaskAfterCollection->Initialize(1.0f, FText::FromString(TEXT("Task after collection")), false, false);
|
||||||
|
TestTrue("A slow task should start after the previous task is collected", TaskAfterCollection->IsActive());
|
||||||
|
TaskAfterCollection->Finish();
|
||||||
|
|
||||||
|
TestTrue(
|
||||||
|
"ShowEditorNotification should create a notification while Slate is active",
|
||||||
|
UDirectiveUtilEditorTaskLibrary::ShowEditorNotification(
|
||||||
|
FText::FromString(TEXT("Directive Utilities test")),
|
||||||
|
EDirectiveUtilEditorNotificationState::Success,
|
||||||
|
0.01f));
|
||||||
|
TestTrue(
|
||||||
|
"ShowEditorNotification should normalize a non-finite duration",
|
||||||
|
UDirectiveUtilEditorTaskLibrary::ShowEditorNotification(
|
||||||
|
FText::FromString(TEXT("Directive Utilities duration test")),
|
||||||
|
EDirectiveUtilEditorNotificationState::Neutral,
|
||||||
|
std::numeric_limits<float>::quiet_NaN()));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -1,41 +1,244 @@
|
|||||||
#include "Libraries/DirectiveUtilFunctionLibrary.h"
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
#include "Tests/AutomationCommon.h"
|
|
||||||
#include "Misc/AutomationTest.h"
|
|
||||||
|
|
||||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilFunctionLibraryTest, "DirectiveUtilities.FunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
#include "Libraries/DirectiveUtilFunctionLibrary.h"
|
||||||
|
#include "Async/Async.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "HAL/PlatformProcess.h"
|
||||||
|
#include "Misc/AutomationTest.h"
|
||||||
|
#include "Misc/App.h"
|
||||||
|
#include "UObject/Package.h"
|
||||||
|
|
||||||
|
namespace DirectiveUtilFunctionLibraryTest
|
||||||
|
{
|
||||||
|
EDirectiveUtilBuildConfiguration GetExpectedBuildConfiguration(const EBuildConfiguration BuildConfiguration)
|
||||||
|
{
|
||||||
|
switch (BuildConfiguration)
|
||||||
|
{
|
||||||
|
case EBuildConfiguration::Unknown:
|
||||||
|
return EDirectiveUtilBuildConfiguration::Unknown;
|
||||||
|
case EBuildConfiguration::Debug:
|
||||||
|
return EDirectiveUtilBuildConfiguration::Debug;
|
||||||
|
case EBuildConfiguration::DebugGame:
|
||||||
|
return EDirectiveUtilBuildConfiguration::DebugGame;
|
||||||
|
case EBuildConfiguration::Development:
|
||||||
|
return EDirectiveUtilBuildConfiguration::Development;
|
||||||
|
case EBuildConfiguration::Shipping:
|
||||||
|
return EDirectiveUtilBuildConfiguration::Shipping;
|
||||||
|
case EBuildConfiguration::Test:
|
||||||
|
return EDirectiveUtilBuildConfiguration::Test;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EDirectiveUtilBuildConfiguration::Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
EDirectiveUtilBuildTargetType GetExpectedBuildTargetType(const EBuildTargetType BuildTargetType)
|
||||||
|
{
|
||||||
|
switch (BuildTargetType)
|
||||||
|
{
|
||||||
|
case EBuildTargetType::Unknown:
|
||||||
|
return EDirectiveUtilBuildTargetType::Unknown;
|
||||||
|
case EBuildTargetType::Game:
|
||||||
|
return EDirectiveUtilBuildTargetType::Game;
|
||||||
|
case EBuildTargetType::Server:
|
||||||
|
return EDirectiveUtilBuildTargetType::Server;
|
||||||
|
case EBuildTargetType::Client:
|
||||||
|
return EDirectiveUtilBuildTargetType::Client;
|
||||||
|
case EBuildTargetType::Editor:
|
||||||
|
return EDirectiveUtilBuildTargetType::Editor;
|
||||||
|
case EBuildTargetType::Program:
|
||||||
|
return EDirectiveUtilBuildTargetType::Program;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EDirectiveUtilBuildTargetType::Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class EClipboardStage : uint8
|
||||||
|
{
|
||||||
|
CopyText,
|
||||||
|
CheckText,
|
||||||
|
CheckString,
|
||||||
|
CheckClear,
|
||||||
|
Complete
|
||||||
|
};
|
||||||
|
|
||||||
|
class FClipboardRoundTripCommand : public IAutomationLatentCommand
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FClipboardRoundTripCommand(FAutomationTestBase* InTest, FString InOriginalClipboard)
|
||||||
|
: Test(InTest)
|
||||||
|
, OriginalClipboard(MoveTemp(InOriginalClipboard))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool Update() override
|
||||||
|
{
|
||||||
|
switch (Stage)
|
||||||
|
{
|
||||||
|
case EClipboardStage::CopyText:
|
||||||
|
UDirectiveUtilFunctionLibrary::CopyTextToClipboard(FText::FromString(TextPayload));
|
||||||
|
BeginRetryWindow(EClipboardStage::CheckText);
|
||||||
|
return false;
|
||||||
|
|
||||||
|
case EClipboardStage::CheckText:
|
||||||
|
if (!HasExpectedValue(UDirectiveUtilFunctionLibrary::GetTextFromClipboard().ToString(), TextPayload, TEXT("GetTextFromClipboard should return the copied text")))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
UDirectiveUtilFunctionLibrary::CopyStringToClipboard(StringPayload);
|
||||||
|
BeginRetryWindow(EClipboardStage::CheckString);
|
||||||
|
return false;
|
||||||
|
|
||||||
|
case EClipboardStage::CheckString:
|
||||||
|
if (!HasExpectedValue(UDirectiveUtilFunctionLibrary::GetStringFromClipboard(), StringPayload, TEXT("GetStringFromClipboard should return the copied string")))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
UDirectiveUtilFunctionLibrary::ClearClipboard();
|
||||||
|
BeginRetryWindow(EClipboardStage::CheckClear);
|
||||||
|
return false;
|
||||||
|
|
||||||
|
case EClipboardStage::CheckClear:
|
||||||
|
if (!HasExpectedValue(UDirectiveUtilFunctionLibrary::GetStringFromClipboard(), FString(), TEXT("GetStringFromClipboard should return an empty string after clearing the clipboard")))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
UDirectiveUtilFunctionLibrary::CopyStringToClipboard(OriginalClipboard);
|
||||||
|
Stage = EClipboardStage::Complete;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case EClipboardStage::Complete:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void BeginRetryWindow(EClipboardStage NextStage)
|
||||||
|
{
|
||||||
|
Stage = NextStage;
|
||||||
|
FramesRemaining = 120;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasExpectedValue(const FString& Actual, const FString& Expected, const TCHAR* FailureMessage)
|
||||||
|
{
|
||||||
|
if (Actual == Expected)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (--FramesRemaining > 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Test->TestEqual(FailureMessage, Actual, Expected);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
FAutomationTestBase* Test;
|
||||||
|
FString OriginalClipboard;
|
||||||
|
EClipboardStage Stage = EClipboardStage::CopyText;
|
||||||
|
int32 FramesRemaining = 0;
|
||||||
|
const FString TextPayload = TEXT("Directive Utilities Text Clipboard");
|
||||||
|
const FString StringPayload = TEXT("Directive Utilities String Clipboard");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilFunctionLibraryTest, "DirectiveUtilities.FunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
bool FDirectiveUtilFunctionLibraryTest::RunTest(const FString& Parameters)
|
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 FString OriginalClipboard = UDirectiveUtilFunctionLibrary::GetStringFromClipboard();
|
||||||
|
ADD_LATENT_AUTOMATION_COMMAND(DirectiveUtilFunctionLibraryTest::FClipboardRoundTripCommand(this, OriginalClipboard));
|
||||||
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();
|
const FString ProjectVersion = UDirectiveUtilFunctionLibrary::GetProjectVersion();
|
||||||
TestNotEqual("GetProjectVersion should return a non-empty string", ProjectVersion, FString(""));
|
TestNotEqual("GetProjectVersion should return a non-empty string", ProjectVersion, FString(""));
|
||||||
|
|
||||||
TestTrue("IsRunningInEditor should return true in editor context",
|
#if WITH_EDITOR
|
||||||
UDirectiveUtilFunctionLibrary::IsRunningInEditor());
|
constexpr bool bExpectedEditorContext = true;
|
||||||
|
#else
|
||||||
|
constexpr bool bExpectedEditorContext = false;
|
||||||
|
#endif
|
||||||
|
TestEqual(
|
||||||
|
"IsRunningInEditor should match the target context",
|
||||||
|
UDirectiveUtilFunctionLibrary::IsRunningInEditor(),
|
||||||
|
bExpectedEditorContext);
|
||||||
|
|
||||||
// Pure pass-through to the engine's GetDerivedClasses (which appends to the output array).
|
struct FWorldTypeCase
|
||||||
|
{
|
||||||
|
EWorldType::Type WorldType;
|
||||||
|
EDirectiveUtilWorldType Expected;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FWorldTypeCase WorldTypeCases[] = {
|
||||||
|
{ EWorldType::None, EDirectiveUtilWorldType::None },
|
||||||
|
{ EWorldType::Game, EDirectiveUtilWorldType::Game },
|
||||||
|
{ EWorldType::Editor, EDirectiveUtilWorldType::Editor },
|
||||||
|
{ EWorldType::PIE, EDirectiveUtilWorldType::PlayInEditor },
|
||||||
|
{ EWorldType::EditorPreview, EDirectiveUtilWorldType::EditorPreview },
|
||||||
|
{ EWorldType::GamePreview, EDirectiveUtilWorldType::GamePreview },
|
||||||
|
{ EWorldType::GameRPC, EDirectiveUtilWorldType::GameRPC },
|
||||||
|
{ EWorldType::Inactive, EDirectiveUtilWorldType::Inactive }
|
||||||
|
};
|
||||||
|
|
||||||
|
UWorld* TestWorld = NewObject<UWorld>();
|
||||||
|
TestNotNull("A transient world should be created for world type tests", TestWorld);
|
||||||
|
if (TestWorld)
|
||||||
|
{
|
||||||
|
for (const FWorldTypeCase& TestCase : WorldTypeCases)
|
||||||
|
{
|
||||||
|
TestWorld->WorldType = TestCase.WorldType;
|
||||||
|
TestEqual(
|
||||||
|
FString::Printf(TEXT("GetWorldType should map %s"), LexToString(TestCase.WorldType)),
|
||||||
|
UDirectiveUtilFunctionLibrary::GetWorldType(TestWorld),
|
||||||
|
TestCase.Expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
TestWorld->WorldType = static_cast<EWorldType::Type>(MAX_uint8);
|
||||||
|
TestEqual(
|
||||||
|
"GetWorldType should reject unsupported world types",
|
||||||
|
UDirectiveUtilFunctionLibrary::GetWorldType(TestWorld),
|
||||||
|
EDirectiveUtilWorldType::Unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
TestEqual(
|
||||||
|
"GetWorldType should return Unknown for a null context",
|
||||||
|
UDirectiveUtilFunctionLibrary::GetWorldType(nullptr),
|
||||||
|
EDirectiveUtilWorldType::Unknown);
|
||||||
|
TestEqual(
|
||||||
|
"GetWorldType should return Unknown when the context has no world",
|
||||||
|
UDirectiveUtilFunctionLibrary::GetWorldType(GetTransientPackage()),
|
||||||
|
EDirectiveUtilWorldType::Unknown);
|
||||||
|
|
||||||
|
TestEqual(
|
||||||
|
"GetBuildConfigurationType should match the application build configuration",
|
||||||
|
UDirectiveUtilFunctionLibrary::GetBuildConfigurationType(),
|
||||||
|
DirectiveUtilFunctionLibraryTest::GetExpectedBuildConfiguration(FApp::GetBuildConfiguration()));
|
||||||
|
TestEqual(
|
||||||
|
"GetBuildTargetType should match the application build target",
|
||||||
|
UDirectiveUtilFunctionLibrary::GetBuildTargetType(),
|
||||||
|
DirectiveUtilFunctionLibraryTest::GetExpectedBuildTargetType(FApp::GetBuildTargetType()));
|
||||||
|
|
||||||
|
TestNotNull("The world type enum should be reflected", StaticEnum<EDirectiveUtilWorldType>());
|
||||||
|
TestNotNull("The build configuration enum should be reflected", StaticEnum<EDirectiveUtilBuildConfiguration>());
|
||||||
|
TestNotNull("The build target type enum should be reflected", StaticEnum<EDirectiveUtilBuildTargetType>());
|
||||||
|
|
||||||
|
const UFunction* GetWorldTypeFunction = UDirectiveUtilFunctionLibrary::StaticClass()->FindFunctionByName(
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilFunctionLibrary, GetWorldType));
|
||||||
|
TestNotNull("GetWorldType should be reflected", GetWorldTypeFunction);
|
||||||
|
if (GetWorldTypeFunction)
|
||||||
|
{
|
||||||
|
TestTrue("GetWorldType should be Blueprint pure", GetWorldTypeFunction->HasAnyFunctionFlags(FUNC_BlueprintPure));
|
||||||
|
TestFalse("GetWorldType should be available at runtime", GetWorldTypeFunction->HasAnyFunctionFlags(FUNC_EditorOnly));
|
||||||
|
#if WITH_EDITOR
|
||||||
|
TestEqual(
|
||||||
|
"GetWorldType should use its input as the Blueprint world context",
|
||||||
|
GetWorldTypeFunction->GetMetaData(TEXT("WorldContext")),
|
||||||
|
FString(TEXT("WorldContextObject")));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
// Happy path (recursive): the running module guarantees a stable hierarchy to query.
|
|
||||||
TArray<UClass*> RecursiveDerived;
|
TArray<UClass*> RecursiveDerived;
|
||||||
UDirectiveUtilFunctionLibrary::GetChildClasses(UBlueprintFunctionLibrary::StaticClass(), true, RecursiveDerived);
|
UDirectiveUtilFunctionLibrary::GetChildClasses(UBlueprintFunctionLibrary::StaticClass(), true, RecursiveDerived);
|
||||||
TestTrue("GetChildClasses should return a non-empty list for a base class with subclasses",
|
TestTrue("GetChildClasses should return a non-empty list for a base class with subclasses",
|
||||||
@@ -45,19 +248,121 @@ bool FDirectiveUtilFunctionLibraryTest::RunTest(const FString& Parameters)
|
|||||||
TestFalse("GetChildClasses should not include the base class itself",
|
TestFalse("GetChildClasses should not include the base class itself",
|
||||||
RecursiveDerived.Contains(UBlueprintFunctionLibrary::StaticClass()));
|
RecursiveDerived.Contains(UBlueprintFunctionLibrary::StaticClass()));
|
||||||
|
|
||||||
// Non-recursive results must not exceed the recursive results for the same base.
|
double ElapsedMilliseconds = 123.0;
|
||||||
|
TestFalse(
|
||||||
|
"StartStopwatch should reject NAME_None",
|
||||||
|
UDirectiveUtilFunctionLibrary::StartStopwatch(NAME_None));
|
||||||
|
TestFalse(
|
||||||
|
"StopStopwatch should reject NAME_None",
|
||||||
|
UDirectiveUtilFunctionLibrary::StopStopwatch(NAME_None, ElapsedMilliseconds));
|
||||||
|
TestEqual("StopStopwatch should reset elapsed time for NAME_None", ElapsedMilliseconds, 0.0);
|
||||||
|
|
||||||
|
const FName MissingStopwatchKey(TEXT("DirectiveUtilMissingStopwatch"));
|
||||||
|
ElapsedMilliseconds = 123.0;
|
||||||
|
TestFalse(
|
||||||
|
"StopStopwatch should reject a missing key",
|
||||||
|
UDirectiveUtilFunctionLibrary::StopStopwatch(MissingStopwatchKey, ElapsedMilliseconds));
|
||||||
|
TestEqual("StopStopwatch should reset elapsed time for a missing key", ElapsedMilliseconds, 0.0);
|
||||||
|
|
||||||
|
const FName BasicStopwatchKey(TEXT("DirectiveUtilBasicStopwatch"));
|
||||||
|
TestTrue(
|
||||||
|
"StartStopwatch should start an unused key",
|
||||||
|
UDirectiveUtilFunctionLibrary::StartStopwatch(BasicStopwatchKey));
|
||||||
|
TestFalse(
|
||||||
|
"StartStopwatch should preserve an active key by default",
|
||||||
|
UDirectiveUtilFunctionLibrary::StartStopwatch(BasicStopwatchKey));
|
||||||
|
TestTrue(
|
||||||
|
"StopStopwatch should stop an active key",
|
||||||
|
UDirectiveUtilFunctionLibrary::StopStopwatch(BasicStopwatchKey, ElapsedMilliseconds));
|
||||||
|
TestTrue("StopStopwatch should return a non-negative duration", ElapsedMilliseconds >= 0.0);
|
||||||
|
TestFalse(
|
||||||
|
"StopStopwatch should consume the active key",
|
||||||
|
UDirectiveUtilFunctionLibrary::StopStopwatch(BasicStopwatchKey, ElapsedMilliseconds));
|
||||||
|
|
||||||
|
const FName RestartStopwatchKey(TEXT("DirectiveUtilRestartStopwatch"));
|
||||||
|
TestTrue(
|
||||||
|
"StartStopwatch should start the restart test key",
|
||||||
|
UDirectiveUtilFunctionLibrary::StartStopwatch(RestartStopwatchKey));
|
||||||
|
TestTrue(
|
||||||
|
"StartStopwatch should replace an active key when requested",
|
||||||
|
UDirectiveUtilFunctionLibrary::StartStopwatch(RestartStopwatchKey, true));
|
||||||
|
TestTrue(
|
||||||
|
"StopStopwatch should stop a restarted key",
|
||||||
|
UDirectiveUtilFunctionLibrary::StopStopwatch(RestartStopwatchKey, ElapsedMilliseconds));
|
||||||
|
|
||||||
|
const FName FirstOverlapKey(TEXT("DirectiveUtilFirstOverlapStopwatch"));
|
||||||
|
const FName SecondOverlapKey(TEXT("DirectiveUtilSecondOverlapStopwatch"));
|
||||||
|
TestTrue(
|
||||||
|
"StartStopwatch should start the first overlapping key",
|
||||||
|
UDirectiveUtilFunctionLibrary::StartStopwatch(FirstOverlapKey));
|
||||||
|
TestTrue(
|
||||||
|
"StartStopwatch should start the second overlapping key",
|
||||||
|
UDirectiveUtilFunctionLibrary::StartStopwatch(SecondOverlapKey));
|
||||||
|
TestTrue(
|
||||||
|
"StopStopwatch should stop the first overlapping key out of order",
|
||||||
|
UDirectiveUtilFunctionLibrary::StopStopwatch(FirstOverlapKey, ElapsedMilliseconds));
|
||||||
|
TestTrue(
|
||||||
|
"StopStopwatch should stop the second overlapping key",
|
||||||
|
UDirectiveUtilFunctionLibrary::StopStopwatch(SecondOverlapKey, ElapsedMilliseconds));
|
||||||
|
|
||||||
|
const FName TimedStopwatchKey(TEXT("DirectiveUtilTimedStopwatch"));
|
||||||
|
TestTrue(
|
||||||
|
"StartStopwatch should start the elapsed-time test key",
|
||||||
|
UDirectiveUtilFunctionLibrary::StartStopwatch(TimedStopwatchKey));
|
||||||
|
FPlatformProcess::SleepNoStats(0.01f);
|
||||||
|
TestTrue(
|
||||||
|
"StopStopwatch should stop the elapsed-time test key",
|
||||||
|
UDirectiveUtilFunctionLibrary::StopStopwatch(TimedStopwatchKey, ElapsedMilliseconds));
|
||||||
|
TestTrue("StopStopwatch should measure elapsed real time in milliseconds", ElapsedMilliseconds >= 5.0);
|
||||||
|
|
||||||
|
const FName CrossThreadStopwatchKey(TEXT("DirectiveUtilCrossThreadStopwatch"));
|
||||||
|
TestTrue(
|
||||||
|
"StartStopwatch should start a key that will stop on another thread",
|
||||||
|
UDirectiveUtilFunctionLibrary::StartStopwatch(CrossThreadStopwatchKey));
|
||||||
|
double CrossThreadElapsedMilliseconds = 0.0;
|
||||||
|
TFuture<bool> CrossThreadStop = Async(EAsyncExecution::ThreadPool, [&CrossThreadElapsedMilliseconds, CrossThreadStopwatchKey]()
|
||||||
|
{
|
||||||
|
return UDirectiveUtilFunctionLibrary::StopStopwatch(
|
||||||
|
CrossThreadStopwatchKey,
|
||||||
|
CrossThreadElapsedMilliseconds);
|
||||||
|
});
|
||||||
|
TestTrue("StopStopwatch should find a key started on another thread", CrossThreadStop.Get());
|
||||||
|
TestTrue("Cross-thread stopwatch duration should be non-negative", CrossThreadElapsedMilliseconds >= 0.0);
|
||||||
|
|
||||||
|
const UFunction* StartStopwatchFunction = UDirectiveUtilFunctionLibrary::StaticClass()->FindFunctionByName(
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilFunctionLibrary, StartStopwatch));
|
||||||
|
const UFunction* StopStopwatchFunction = UDirectiveUtilFunctionLibrary::StaticClass()->FindFunctionByName(
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilFunctionLibrary, StopStopwatch));
|
||||||
|
TestNotNull("StartStopwatch should be reflected", StartStopwatchFunction);
|
||||||
|
TestNotNull("StopStopwatch should be reflected", StopStopwatchFunction);
|
||||||
|
for (const UFunction* StopwatchFunction : { StartStopwatchFunction, StopStopwatchFunction })
|
||||||
|
{
|
||||||
|
if (!StopwatchFunction)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestTrue("Stopwatch functions should be Blueprint callable", StopwatchFunction->HasAnyFunctionFlags(FUNC_BlueprintCallable));
|
||||||
|
TestFalse("Stopwatch functions should not be Blueprint pure", StopwatchFunction->HasAnyFunctionFlags(FUNC_BlueprintPure));
|
||||||
|
TestFalse("Stopwatch functions should be available at runtime", StopwatchFunction->HasAnyFunctionFlags(FUNC_EditorOnly));
|
||||||
|
#if WITH_EDITOR
|
||||||
|
TestEqual(
|
||||||
|
"Stopwatch functions should use the profiling category",
|
||||||
|
StopwatchFunction->GetMetaData(TEXT("Category")),
|
||||||
|
FString(TEXT("Directive Utilities|Utility|Profiling")));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
TArray<UClass*> NonRecursiveDerived;
|
TArray<UClass*> NonRecursiveDerived;
|
||||||
UDirectiveUtilFunctionLibrary::GetChildClasses(UBlueprintFunctionLibrary::StaticClass(), false, NonRecursiveDerived);
|
UDirectiveUtilFunctionLibrary::GetChildClasses(UBlueprintFunctionLibrary::StaticClass(), false, NonRecursiveDerived);
|
||||||
TestTrue("GetChildClasses non-recursive count should not exceed recursive count",
|
TestTrue("GetChildClasses non-recursive count should not exceed recursive count",
|
||||||
NonRecursiveDerived.Num() <= RecursiveDerived.Num());
|
NonRecursiveDerived.Num() <= RecursiveDerived.Num());
|
||||||
|
|
||||||
// Leaf class with no subclasses: an empty input array should remain empty.
|
|
||||||
TArray<UClass*> LeafDerived;
|
TArray<UClass*> LeafDerived;
|
||||||
UDirectiveUtilFunctionLibrary::GetChildClasses(UDirectiveUtilFunctionLibrary::StaticClass(), true, LeafDerived);
|
UDirectiveUtilFunctionLibrary::GetChildClasses(UDirectiveUtilFunctionLibrary::StaticClass(), true, LeafDerived);
|
||||||
TestEqual("GetChildClasses should return an empty list for a class with no subclasses",
|
TestEqual("GetChildClasses should return an empty list for a class with no subclasses",
|
||||||
LeafDerived.Num(), 0);
|
LeafDerived.Num(), 0);
|
||||||
|
|
||||||
// Null base class: should not crash and should add nothing.
|
|
||||||
TArray<UClass*> NullBaseDerived;
|
TArray<UClass*> NullBaseDerived;
|
||||||
UDirectiveUtilFunctionLibrary::GetChildClasses(nullptr, true, NullBaseDerived);
|
UDirectiveUtilFunctionLibrary::GetChildClasses(nullptr, true, NullBaseDerived);
|
||||||
TestEqual("GetChildClasses should return an empty list for a null base class",
|
TestEqual("GetChildClasses should return an empty list for a null base class",
|
||||||
@@ -87,11 +392,9 @@ bool FDirectiveUtilFunctionLibraryTest::RunTest(const FString& Parameters)
|
|||||||
TestFalse("GetCommandLineOption should return false for an empty key",
|
TestFalse("GetCommandLineOption should return false for an empty key",
|
||||||
UDirectiveUtilFunctionLibrary::GetCommandLineOption(CommandLine, TEXT(""), Value));
|
UDirectiveUtilFunctionLibrary::GetCommandLineOption(CommandLine, TEXT(""), Value));
|
||||||
|
|
||||||
// Smoke test through the process-command-line path.
|
|
||||||
TestFalse("HasCommandLineSwitch should not find a switch that was never passed",
|
TestFalse("HasCommandLineSwitch should not find a switch that was never passed",
|
||||||
UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(TEXT("DirectiveUtilitiesDefinitelyNotPassed")));
|
UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(TEXT("DirectiveUtilitiesDefinitelyNotPassed")));
|
||||||
}
|
}
|
||||||
|
|
||||||
UDirectiveUtilFunctionLibrary::CopyStringToClipboard(OriginalClipboard);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
#include "Libraries/DirectiveUtilGameplayTagFunctionLibrary.h"
|
#include "Libraries/DirectiveUtilGameplayTagFunctionLibrary.h"
|
||||||
#include "GameplayTagsManager.h"
|
#include "GameplayTagsManager.h"
|
||||||
#include "Misc/AutomationTest.h"
|
#include "Misc/AutomationTest.h"
|
||||||
#include "Misc/FileHelper.h"
|
#include "Misc/FileHelper.h"
|
||||||
#include "Misc/Paths.h"
|
#include "Misc/Paths.h"
|
||||||
|
|
||||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilGameplayTagFunctionLibraryTest, "DirectiveUtilities.GameplayTagFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilGameplayTagFunctionLibraryTest, "DirectiveUtilities.GameplayTagFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
bool FDirectiveUtilGameplayTagFunctionLibraryTest::RunTest(const FString& Parameters)
|
bool FDirectiveUtilGameplayTagFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||||
{
|
{
|
||||||
@@ -109,5 +111,23 @@ bool FDirectiveUtilGameplayTagFunctionLibraryTest::RunTest(const FString& Parame
|
|||||||
TestEqual("FindRegisteredTags with no match should be empty", UDirectiveUtilGameplayTagFunctionLibrary::FindRegisteredTags(TEXT("DirectiveUtilitiesNoSuchTagXYZ")).Num(), 0);
|
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);
|
TestEqual("FindRegisteredTags with an empty substring should be empty", UDirectiveUtilGameplayTagFunctionLibrary::FindRegisteredTags(TEXT("")).Num(), 0);
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
const TArray<FName> RegistryQueryFunctions = {
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilGameplayTagFunctionLibrary, GetTagParents),
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilGameplayTagFunctionLibrary, GetTagChildren),
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilGameplayTagFunctionLibrary, GetTagDirectChildren),
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilGameplayTagFunctionLibrary, IsLeafTag)
|
||||||
|
};
|
||||||
|
for (const FName FunctionName : RegistryQueryFunctions)
|
||||||
|
{
|
||||||
|
const UFunction* Function = UDirectiveUtilGameplayTagFunctionLibrary::StaticClass()->FindFunctionByName(FunctionName);
|
||||||
|
TestNotNull(*FString::Printf(TEXT("%s should be reflected"), *FunctionName.ToString()), Function);
|
||||||
|
if (Function)
|
||||||
|
{
|
||||||
|
TestFalse(*FString::Printf(TEXT("%s should not advertise worker-thread safety"), *FunctionName.ToString()), Function->HasMetaData(TEXT("BlueprintThreadSafe")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "Types/DirectiveUtilInputTypes.h"
|
#include "Types/DirectiveUtilInputTypes.h"
|
||||||
#include "Types/DirectiveUtilTypes.h"
|
#include "Types/DirectiveUtilTypes.h"
|
||||||
#include "InputMappingContext.h"
|
#include "InputMappingContext.h"
|
||||||
|
#include "EnhancedPlayerInput.h"
|
||||||
#include "EnhancedInputSubsystems.h"
|
#include "EnhancedInputSubsystems.h"
|
||||||
#include "Engine/GameInstance.h"
|
#include "Engine/GameInstance.h"
|
||||||
#include "Engine/LocalPlayer.h"
|
#include "Engine/LocalPlayer.h"
|
||||||
@@ -12,34 +13,24 @@
|
|||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
#include "Misc/AutomationTest.h"
|
#include "Misc/AutomationTest.h"
|
||||||
|
|
||||||
#if WITH_EDITOR
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilInputActivePathTest, "DirectiveUtilities.InputActivePathTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
/**
|
|
||||||
* 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)
|
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("LocalPlayer not found. Cannot set input mapping contexts."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||||
AddExpectedMessagePlain(TEXT("EnhancedInput subsystem not found."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
AddExpectedMessagePlain(TEXT("EnhancedInput subsystem not found."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||||
|
|
||||||
UGameInstance* GameInstance = NewObject<UGameInstance>(GEngine);
|
UGameInstance* GameInstance = NewObject<UGameInstance>(GEngine);
|
||||||
if (!GameInstance)
|
if (!GameInstance)
|
||||||
{
|
{
|
||||||
AddInfo(TEXT("Could not create a game instance; skipping Enhanced Input active-path assertions."));
|
AddError(TEXT("Could not create a game instance for Enhanced Input active-path assertions."));
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
GameInstance->AddToRoot();
|
GameInstance->AddToRoot();
|
||||||
GameInstance->InitializeStandalone();
|
GameInstance->InitializeStandalone();
|
||||||
|
|
||||||
UWorld* World = GameInstance->GetWorld();
|
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();
|
UClass* LocalPlayerClass = GEngine->LocalPlayerClass ? GEngine->LocalPlayerClass.Get() : ULocalPlayer::StaticClass();
|
||||||
ULocalPlayer* LocalPlayer = NewObject<ULocalPlayer>(GEngine, LocalPlayerClass);
|
ULocalPlayer* LocalPlayer = NewObject<ULocalPlayer>(GEngine, LocalPlayerClass);
|
||||||
if (World && LocalPlayer)
|
if (World && LocalPlayer)
|
||||||
@@ -49,33 +40,40 @@ bool FDirectiveUtilInputActivePathTest::RunTest(const FString& Parameters)
|
|||||||
|
|
||||||
if (!World || !LocalPlayer)
|
if (!World || !LocalPlayer)
|
||||||
{
|
{
|
||||||
AddInfo(TEXT("Local player unavailable in the headless harness; skipping Enhanced Input active-path assertions."));
|
AddError(TEXT("Local player unavailable in the Enhanced Input test harness."));
|
||||||
GameInstance->Shutdown();
|
GameInstance->Shutdown();
|
||||||
GameInstance->RemoveFromRoot();
|
GameInstance->RemoveFromRoot();
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
APlayerController* PlayerController = World->SpawnActor<APlayerController>();
|
APlayerController* PlayerController = World->SpawnActor<APlayerController>();
|
||||||
if (!PlayerController)
|
if (!PlayerController)
|
||||||
{
|
{
|
||||||
AddInfo(TEXT("Could not spawn a player controller; skipping Enhanced Input active-path assertions."));
|
AddError(TEXT("Could not spawn a player controller for Enhanced Input active-path assertions."));
|
||||||
GameInstance->Shutdown();
|
GameInstance->Shutdown();
|
||||||
GameInstance->RemoveFromRoot();
|
GameInstance->RemoveFromRoot();
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
PlayerController->SetPlayer(LocalPlayer);
|
PlayerController->SetPlayer(LocalPlayer);
|
||||||
|
PlayerController->InitInputSystem();
|
||||||
|
PlayerController->PlayerInput = NewObject<UEnhancedPlayerInput>(PlayerController);
|
||||||
|
|
||||||
UEnhancedInputLocalPlayerSubsystem* Subsystem = UDirectiveUtilInputFunctionLibrary::GetEnhancedInputSubsystem(PlayerController);
|
UEnhancedInputLocalPlayerSubsystem* Subsystem = UDirectiveUtilInputFunctionLibrary::GetEnhancedInputSubsystem(PlayerController);
|
||||||
if (!Subsystem)
|
if (!Subsystem)
|
||||||
{
|
{
|
||||||
AddInfo(TEXT("Enhanced Input subsystem unavailable for the synthetic local player; skipping active-path assertions."));
|
AddError(TEXT("Enhanced Input subsystem unavailable for the synthetic local player."));
|
||||||
GameInstance->Shutdown();
|
GameInstance->Shutdown();
|
||||||
GameInstance->RemoveFromRoot();
|
GameInstance->RemoveFromRoot();
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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);
|
TestNotNull("GetEnhancedInputSubsystem returns the subsystem for a live local player", Subsystem);
|
||||||
|
auto ApplyPendingMappings = [Subsystem]
|
||||||
|
{
|
||||||
|
FModifyContextOptions Options;
|
||||||
|
Options.bForceImmediately = true;
|
||||||
|
Subsystem->RequestRebuildControlMappings(Options);
|
||||||
|
};
|
||||||
|
|
||||||
UInputMappingContext* ContextA = NewObject<UInputMappingContext>(GetTransientPackage());
|
UInputMappingContext* ContextA = NewObject<UInputMappingContext>(GetTransientPackage());
|
||||||
UInputMappingContext* ContextB = NewObject<UInputMappingContext>(GetTransientPackage());
|
UInputMappingContext* ContextB = NewObject<UInputMappingContext>(GetTransientPackage());
|
||||||
@@ -84,7 +82,6 @@ bool FDirectiveUtilInputActivePathTest::RunTest(const FString& Parameters)
|
|||||||
const TSoftObjectPtr<UInputMappingContext> SoftA(ContextA);
|
const TSoftObjectPtr<UInputMappingContext> SoftA(ContextA);
|
||||||
const TSoftObjectPtr<UInputMappingContext> SoftB(ContextB);
|
const TSoftObjectPtr<UInputMappingContext> SoftB(ContextB);
|
||||||
|
|
||||||
// Add context A.
|
|
||||||
FDirectiveUtilEnhancedInputContextData DataA;
|
FDirectiveUtilEnhancedInputContextData DataA;
|
||||||
DataA.InputContext = SoftA;
|
DataA.InputContext = SoftA;
|
||||||
DataA.Priority = 0;
|
DataA.Priority = 0;
|
||||||
@@ -93,38 +90,47 @@ bool FDirectiveUtilInputActivePathTest::RunTest(const FString& Parameters)
|
|||||||
TestEqual("AddInputMappingContexts succeeds for a live controller",
|
TestEqual("AddInputMappingContexts succeeds for a live controller",
|
||||||
UDirectiveUtilInputFunctionLibrary::AddInputMappingContexts(PlayerController, ToAdd, false),
|
UDirectiveUtilInputFunctionLibrary::AddInputMappingContexts(PlayerController, ToAdd, false),
|
||||||
EDirectiveUtilSuccessStatus::Success);
|
EDirectiveUtilSuccessStatus::Success);
|
||||||
|
ApplyPendingMappings();
|
||||||
TestTrue("Added context is reported active",
|
TestTrue("Added context is reported active",
|
||||||
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftA));
|
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftA));
|
||||||
|
|
||||||
// Swap A -> B.
|
|
||||||
TestEqual("SwapInputMappingContexts succeeds",
|
TestEqual("SwapInputMappingContexts succeeds",
|
||||||
UDirectiveUtilInputFunctionLibrary::SwapInputMappingContexts(PlayerController, SoftA, SoftB, 0, false),
|
UDirectiveUtilInputFunctionLibrary::SwapInputMappingContexts(PlayerController, SoftA, SoftB, 0, false),
|
||||||
EDirectiveUtilSuccessStatus::Success);
|
EDirectiveUtilSuccessStatus::Success);
|
||||||
|
ApplyPendingMappings();
|
||||||
TestFalse("Swapped-out context is inactive",
|
TestFalse("Swapped-out context is inactive",
|
||||||
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftA));
|
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftA));
|
||||||
TestTrue("Swapped-in context is active",
|
TestTrue("Swapped-in context is active",
|
||||||
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftB));
|
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftB));
|
||||||
|
|
||||||
// Remove B.
|
|
||||||
TArray<TSoftObjectPtr<UInputMappingContext>> ToRemove;
|
TArray<TSoftObjectPtr<UInputMappingContext>> ToRemove;
|
||||||
ToRemove.Add(SoftB);
|
ToRemove.Add(SoftB);
|
||||||
TestEqual("RemoveInputMappingContexts succeeds",
|
TestEqual("RemoveInputMappingContexts succeeds",
|
||||||
UDirectiveUtilInputFunctionLibrary::RemoveInputMappingContexts(PlayerController, ToRemove),
|
UDirectiveUtilInputFunctionLibrary::RemoveInputMappingContexts(PlayerController, ToRemove),
|
||||||
EDirectiveUtilSuccessStatus::Success);
|
EDirectiveUtilSuccessStatus::Success);
|
||||||
|
ApplyPendingMappings();
|
||||||
TestFalse("Removed context is inactive",
|
TestFalse("Removed context is inactive",
|
||||||
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftB));
|
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftB));
|
||||||
|
TestEqual("Swap adds the new context when the previous context is inactive",
|
||||||
|
UDirectiveUtilInputFunctionLibrary::SwapInputMappingContexts(PlayerController, SoftA, SoftB, 7, true),
|
||||||
|
EDirectiveUtilSuccessStatus::Success);
|
||||||
|
ApplyPendingMappings();
|
||||||
|
TestTrue("Fallback swap context is active",
|
||||||
|
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftB));
|
||||||
|
UDirectiveUtilInputFunctionLibrary::RemoveInputMappingContexts(PlayerController, {SoftB});
|
||||||
|
ApplyPendingMappings();
|
||||||
|
|
||||||
// Clear all (after re-adding A).
|
|
||||||
UDirectiveUtilInputFunctionLibrary::AddInputMappingContexts(PlayerController, ToAdd, false);
|
UDirectiveUtilInputFunctionLibrary::AddInputMappingContexts(PlayerController, ToAdd, false);
|
||||||
TestEqual("ClearAllInputMappingContexts succeeds",
|
TestEqual("ClearAllInputMappingContexts succeeds",
|
||||||
UDirectiveUtilInputFunctionLibrary::ClearAllInputMappingContexts(PlayerController),
|
UDirectiveUtilInputFunctionLibrary::ClearAllInputMappingContexts(PlayerController),
|
||||||
EDirectiveUtilSuccessStatus::Success);
|
EDirectiveUtilSuccessStatus::Success);
|
||||||
|
ApplyPendingMappings();
|
||||||
TestFalse("Context is inactive after clear-all",
|
TestFalse("Context is inactive after clear-all",
|
||||||
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftA));
|
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftA));
|
||||||
|
|
||||||
// A clearing add whose every context fails to load must leave the existing mappings untouched.
|
AddExpectedMessagePlain(TEXT("input mapping contexts could not be loaded"), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||||
AddExpectedMessagePlain(TEXT("Input Mapping Contexts failed to load and were not added!"), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
|
||||||
UDirectiveUtilInputFunctionLibrary::AddInputMappingContexts(PlayerController, ToAdd, false);
|
UDirectiveUtilInputFunctionLibrary::AddInputMappingContexts(PlayerController, ToAdd, false);
|
||||||
|
ApplyPendingMappings();
|
||||||
FDirectiveUtilEnhancedInputContextData UnresolvableData;
|
FDirectiveUtilEnhancedInputContextData UnresolvableData;
|
||||||
UnresolvableData.Priority = 0;
|
UnresolvableData.Priority = 0;
|
||||||
TArray<FDirectiveUtilEnhancedInputContextData> UnresolvableToAdd;
|
TArray<FDirectiveUtilEnhancedInputContextData> UnresolvableToAdd;
|
||||||
@@ -142,5 +148,3 @@ bool FDirectiveUtilInputActivePathTest::RunTest(const FString& Parameters)
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // WITH_EDITOR
|
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
#include "Libraries/DirectiveUtilInputFunctionLibrary.h"
|
#include "Libraries/DirectiveUtilInputFunctionLibrary.h"
|
||||||
#include "Types/DirectiveUtilTypes.h"
|
#include "Types/DirectiveUtilTypes.h"
|
||||||
#include "InputMappingContext.h"
|
#include "InputMappingContext.h"
|
||||||
#include "Misc/AutomationTest.h"
|
#include "Misc/AutomationTest.h"
|
||||||
|
|
||||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilInputFunctionLibraryTest, "DirectiveUtilities.InputFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilInputFunctionLibraryTest, "DirectiveUtilities.InputFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
bool FDirectiveUtilInputFunctionLibraryTest::RunTest(const FString& Parameters)
|
bool FDirectiveUtilInputFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||||
{
|
{
|
||||||
// The null-controller and invalid-context paths intentionally log warnings. Register them as
|
// 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.
|
// 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("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
|
// AddInputMappingContexts should return Failure with null controller
|
||||||
TArray<FDirectiveUtilEnhancedInputContextData> Contexts;
|
TArray<FDirectiveUtilEnhancedInputContextData> Contexts;
|
||||||
@@ -39,8 +40,6 @@ bool FDirectiveUtilInputFunctionLibraryTest::RunTest(const FString& Parameters)
|
|||||||
UDirectiveUtilInputFunctionLibrary::RemoveInputMappingContexts(nullptr, EmptyRemoveContexts),
|
UDirectiveUtilInputFunctionLibrary::RemoveInputMappingContexts(nullptr, EmptyRemoveContexts),
|
||||||
EDirectiveUtilSuccessStatus::Failure);
|
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;
|
const TSoftObjectPtr<UInputMappingContext> NullContext;
|
||||||
TestEqual("SwapInputMappingContexts should fail when both contexts are invalid",
|
TestEqual("SwapInputMappingContexts should fail when both contexts are invalid",
|
||||||
UDirectiveUtilInputFunctionLibrary::SwapInputMappingContexts(nullptr, NullContext, NullContext, 0, false),
|
UDirectiveUtilInputFunctionLibrary::SwapInputMappingContexts(nullptr, NullContext, NullContext, 0, false),
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
#include "Libraries/DirectiveUtilMapFunctionLibrary.h"
|
#include "Libraries/DirectiveUtilMapFunctionLibrary.h"
|
||||||
#include "Tests/DirectiveUtilTestObject.h"
|
#include "Tests/DirectiveUtilTestObject.h"
|
||||||
#include "Misc/AutomationTest.h"
|
#include "Misc/AutomationTest.h"
|
||||||
|
|
||||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMapFunctionLibraryTest, "DirectiveUtilities.MapFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMapFunctionLibraryTest, "DirectiveUtilities.MapFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
bool FDirectiveUtilMapFunctionLibraryTest::RunTest(const FString& Parameters)
|
bool FDirectiveUtilMapFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||||
{
|
{
|
||||||
@@ -144,5 +146,11 @@ bool FDirectiveUtilMapFunctionLibraryTest::RunTest(const FString& Parameters)
|
|||||||
TestEqual("Append with null or mismatched properties should preserve the value", *UntouchedValue, 10);
|
TestEqual("Append with null or mismatched properties should preserve the value", *UntouchedValue, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TestObject->TestMap = {{1, 10}, {2, 20}};
|
||||||
|
UDirectiveUtilMapFunctionLibrary::GenericMap_Append(&TestObject->TestMap, MapProperty, &TestObject->TestMap, MapProperty, true);
|
||||||
|
TestEqual("Append with the same map should preserve its size", TestObject->TestMap.Num(), 2);
|
||||||
|
TestEqual("Append with the same map should preserve the first value", TestObject->TestMap.FindRef(1), 10);
|
||||||
|
TestEqual("Append with the same map should preserve the second value", TestObject->TestMap.FindRef(2), 20);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,342 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "Misc/AutomationTest.h"
|
||||||
|
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "EdGraphSchema_K2.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "K2Node_CallFunction.h"
|
||||||
|
#include "K2Node_TemporaryVariable.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "Kismet2/CompilerResultsLog.h"
|
||||||
|
#include "Libraries/DirectiveUtilMapFunctionLibrary.h"
|
||||||
|
#include "Nodes/DirectiveUtilMapNodeMigration.h"
|
||||||
|
#include "Nodes/K2Node_DirectiveUtilMapAppend.h"
|
||||||
|
#include "Tests/DirectiveUtilTestObject.h"
|
||||||
|
|
||||||
|
namespace DirectiveUtilMapNodeTest
|
||||||
|
{
|
||||||
|
struct FDependentPin
|
||||||
|
{
|
||||||
|
FName Name;
|
||||||
|
FName Category;
|
||||||
|
EPinContainerType ContainerType;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FMapNodeCase
|
||||||
|
{
|
||||||
|
FName FunctionName;
|
||||||
|
TArray<FDependentPin> DependentPins;
|
||||||
|
};
|
||||||
|
|
||||||
|
FEdGraphPinType MakeMapType(
|
||||||
|
const FName KeyCategory,
|
||||||
|
const FName ValueCategory,
|
||||||
|
UObject* KeySubCategoryObject = nullptr,
|
||||||
|
UObject* ValueSubCategoryObject = nullptr)
|
||||||
|
{
|
||||||
|
FEdGraphPinType PinType;
|
||||||
|
PinType.PinCategory = KeyCategory;
|
||||||
|
PinType.PinSubCategoryObject = KeySubCategoryObject;
|
||||||
|
PinType.PinValueType.TerminalCategory = ValueCategory;
|
||||||
|
PinType.PinValueType.TerminalSubCategoryObject = ValueSubCategoryObject;
|
||||||
|
PinType.ContainerType = EPinContainerType::Map;
|
||||||
|
return PinType;
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraph* MakeGraph()
|
||||||
|
{
|
||||||
|
UBlueprint* Blueprint = NewObject<UBlueprint>();
|
||||||
|
Blueprint->GeneratedClass = UObject::StaticClass();
|
||||||
|
Blueprint->SkeletonGeneratedClass = UObject::StaticClass();
|
||||||
|
UEdGraph* Graph = NewObject<UEdGraph>(Blueprint);
|
||||||
|
Graph->Schema = UEdGraphSchema_K2::StaticClass();
|
||||||
|
Blueprint->UbergraphPages.Add(Graph);
|
||||||
|
return Graph;
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraphPin* AddMapOutput(UEdGraph& Graph, const FName Name, const FEdGraphPinType& PinType)
|
||||||
|
{
|
||||||
|
UK2Node_TemporaryVariable* SourceNode = NewObject<UK2Node_TemporaryVariable>(&Graph);
|
||||||
|
SourceNode->VariableType = PinType;
|
||||||
|
Graph.AddNode(SourceNode);
|
||||||
|
SourceNode->AllocateDefaultPins();
|
||||||
|
UEdGraphPin* OutputPin = SourceNode->GetVariablePin();
|
||||||
|
OutputPin->PinName = Name;
|
||||||
|
return OutputPin;
|
||||||
|
}
|
||||||
|
|
||||||
|
UK2Node_CallFunction* AddFunctionNode(UEdGraph& Graph, const FName FunctionName)
|
||||||
|
{
|
||||||
|
UK2Node_CallFunction* Node = NewObject<UK2Node_CallFunction>(&Graph);
|
||||||
|
Graph.AddNode(Node);
|
||||||
|
const UFunction* Function = UDirectiveUtilMapFunctionLibrary::StaticClass()->FindFunctionByName(FunctionName);
|
||||||
|
Node->FunctionReference.SetExternalMember(FunctionName, UDirectiveUtilMapFunctionLibrary::StaticClass());
|
||||||
|
Node->bDefaultsToPureFunc = Function && Function->HasAnyFunctionFlags(FUNC_BlueprintPure);
|
||||||
|
Node->AllocateDefaultPins();
|
||||||
|
return Node;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Connect(UEdGraphPin& OutputPin, UK2Node& Node, UEdGraphPin& InputPin)
|
||||||
|
{
|
||||||
|
OutputPin.MakeLinkTo(&InputPin);
|
||||||
|
Node.PinConnectionListChanged(&InputPin);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Disconnect(UEdGraphPin& OutputPin, UK2Node& Node, UEdGraphPin& InputPin)
|
||||||
|
{
|
||||||
|
OutputPin.BreakLinkTo(&InputPin);
|
||||||
|
Node.PinConnectionListChanged(&InputPin);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasMapType(const UEdGraphPin& Pin, const FName KeyCategory, const FName ValueCategory)
|
||||||
|
{
|
||||||
|
return Pin.PinType.IsMap()
|
||||||
|
&& Pin.PinType.PinCategory == KeyCategory
|
||||||
|
&& Pin.PinType.PinValueType.TerminalCategory == ValueCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasMapType(const UEdGraphPin& Pin, const FEdGraphPinType& ExpectedType)
|
||||||
|
{
|
||||||
|
return Pin.PinType.IsMap()
|
||||||
|
&& Pin.PinType.PinCategory == ExpectedType.PinCategory
|
||||||
|
&& Pin.PinType.PinSubCategory == ExpectedType.PinSubCategory
|
||||||
|
&& Pin.PinType.PinSubCategoryObject == ExpectedType.PinSubCategoryObject
|
||||||
|
&& Pin.PinType.PinValueType == ExpectedType.PinValueType;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasElementType(
|
||||||
|
const UEdGraphPin& Pin,
|
||||||
|
const FName Category,
|
||||||
|
UObject* SubCategoryObject,
|
||||||
|
const EPinContainerType ContainerType)
|
||||||
|
{
|
||||||
|
return Pin.PinType.PinCategory == Category
|
||||||
|
&& Pin.PinType.PinSubCategoryObject == SubCategoryObject
|
||||||
|
&& Pin.PinType.ContainerType == ContainerType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
|
FDirectiveUtilMapNodeWildcardTest,
|
||||||
|
"DirectiveUtilities.MapNodeWildcardTests",
|
||||||
|
EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FDirectiveUtilMapNodeWildcardTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
using namespace DirectiveUtilMapNodeTest;
|
||||||
|
|
||||||
|
const TArray<FMapNodeCase> NodeCases = {
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMapFunctionLibrary, Map_FindOrAdd), {{TEXT("Key"), UEdGraphSchema_K2::PC_String, EPinContainerType::None}, {TEXT("Value"), UEdGraphSchema_K2::PC_Int, EPinContainerType::None}}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMapFunctionLibrary, Map_ClearValues), {}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMapFunctionLibrary, Map_GetKeysByValue), {{TEXT("Value"), UEdGraphSchema_K2::PC_Int, EPinContainerType::None}, {TEXT("Keys"), UEdGraphSchema_K2::PC_String, EPinContainerType::Array}}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMapFunctionLibrary, Map_HasValue), {{TEXT("Value"), UEdGraphSchema_K2::PC_Int, EPinContainerType::None}}},
|
||||||
|
{GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMapFunctionLibrary, Map_RemoveKeys), {{TEXT("Keys"), UEdGraphSchema_K2::PC_String, EPinContainerType::Array}}},
|
||||||
|
};
|
||||||
|
|
||||||
|
const FEdGraphPinType StringIntMap = MakeMapType(UEdGraphSchema_K2::PC_String, UEdGraphSchema_K2::PC_Int);
|
||||||
|
for (const FMapNodeCase& NodeCase : NodeCases)
|
||||||
|
{
|
||||||
|
UEdGraph* Graph = MakeGraph();
|
||||||
|
UK2Node_CallFunction* Node = AddFunctionNode(*Graph, NodeCase.FunctionName);
|
||||||
|
UEdGraphPin* TargetMap = Node->FindPin(TEXT("TargetMap"));
|
||||||
|
TestNotNull(*FString::Printf(TEXT("%s should have a TargetMap pin"), *NodeCase.FunctionName.ToString()), TargetMap);
|
||||||
|
if (!TargetMap)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestTrue(*FString::Printf(TEXT("%s should begin as a wildcard map"), *NodeCase.FunctionName.ToString()), HasMapType(*TargetMap, UEdGraphSchema_K2::PC_Wildcard, UEdGraphSchema_K2::PC_Wildcard));
|
||||||
|
UEdGraphPin* MapOutput = AddMapOutput(*Graph, TEXT("MapOutput"), StringIntMap);
|
||||||
|
Connect(*MapOutput, *Node, *TargetMap);
|
||||||
|
TestTrue(*FString::Printf(TEXT("%s should resolve its map type"), *NodeCase.FunctionName.ToString()), HasMapType(*TargetMap, UEdGraphSchema_K2::PC_String, UEdGraphSchema_K2::PC_Int));
|
||||||
|
|
||||||
|
for (const FDependentPin& ExpectedPin : NodeCase.DependentPins)
|
||||||
|
{
|
||||||
|
const UEdGraphPin* DependentPin = Node->FindPin(ExpectedPin.Name);
|
||||||
|
TestNotNull(*FString::Printf(TEXT("%s should have a %s pin"), *NodeCase.FunctionName.ToString(), *ExpectedPin.Name.ToString()), DependentPin);
|
||||||
|
if (DependentPin)
|
||||||
|
{
|
||||||
|
TestEqual(*FString::Printf(TEXT("%s should resolve %s"), *NodeCase.FunctionName.ToString(), *ExpectedPin.Name.ToString()), DependentPin->PinType.PinCategory, ExpectedPin.Category);
|
||||||
|
TestEqual(*FString::Printf(TEXT("%s should preserve the %s container"), *NodeCase.FunctionName.ToString(), *ExpectedPin.Name.ToString()), DependentPin->PinType.ContainerType, ExpectedPin.ContainerType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Disconnect(*MapOutput, *Node, *TargetMap);
|
||||||
|
TestTrue(*FString::Printf(TEXT("%s should reset after disconnect"), *NodeCase.FunctionName.ToString()), HasMapType(*TargetMap, UEdGraphSchema_K2::PC_Wildcard, UEdGraphSchema_K2::PC_Wildcard));
|
||||||
|
for (const FDependentPin& ExpectedPin : NodeCase.DependentPins)
|
||||||
|
{
|
||||||
|
const UEdGraphPin* DependentPin = Node->FindPin(ExpectedPin.Name);
|
||||||
|
if (DependentPin)
|
||||||
|
{
|
||||||
|
TestEqual(*FString::Printf(TEXT("%s should reset %s after disconnect"), *NodeCase.FunctionName.ToString(), *ExpectedPin.Name.ToString()), DependentPin->PinType.PinCategory, UEdGraphSchema_K2::PC_Wildcard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const FEdGraphPinType ObjectStructMap = MakeMapType(
|
||||||
|
UEdGraphSchema_K2::PC_Object,
|
||||||
|
UEdGraphSchema_K2::PC_Struct,
|
||||||
|
UDirectiveUtilTestObject::StaticClass(),
|
||||||
|
FDirectiveUtilCollisionValue::StaticStruct());
|
||||||
|
for (const FMapNodeCase& NodeCase : NodeCases)
|
||||||
|
{
|
||||||
|
UEdGraph* Graph = MakeGraph();
|
||||||
|
UK2Node_CallFunction* Node = AddFunctionNode(*Graph, NodeCase.FunctionName);
|
||||||
|
UEdGraphPin* TargetMap = Node->FindPin(TEXT("TargetMap"));
|
||||||
|
if (!TestNotNull(*FString::Printf(TEXT("%s should have a typed TargetMap pin"), *NodeCase.FunctionName.ToString()), TargetMap))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraphPin* MapOutput = AddMapOutput(*Graph, TEXT("ObjectStructMapOutput"), ObjectStructMap);
|
||||||
|
Connect(*MapOutput, *Node, *TargetMap);
|
||||||
|
TestTrue(
|
||||||
|
*FString::Printf(TEXT("%s should preserve map subtype objects"), *NodeCase.FunctionName.ToString()),
|
||||||
|
HasMapType(*TargetMap, ObjectStructMap));
|
||||||
|
for (const FDependentPin& ExpectedPin : NodeCase.DependentPins)
|
||||||
|
{
|
||||||
|
const UEdGraphPin* DependentPin = Node->FindPin(ExpectedPin.Name);
|
||||||
|
if (!DependentPin)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const bool bUsesKeyType = ExpectedPin.Name == TEXT("Key") || ExpectedPin.Name == TEXT("Keys");
|
||||||
|
TestTrue(
|
||||||
|
*FString::Printf(TEXT("%s should preserve %s subtype"), *NodeCase.FunctionName.ToString(), *ExpectedPin.Name.ToString()),
|
||||||
|
HasElementType(
|
||||||
|
*DependentPin,
|
||||||
|
bUsesKeyType ? UEdGraphSchema_K2::PC_Object : UEdGraphSchema_K2::PC_Struct,
|
||||||
|
bUsesKeyType
|
||||||
|
? static_cast<UObject*>(UDirectiveUtilTestObject::StaticClass())
|
||||||
|
: static_cast<UObject*>(FDirectiveUtilCollisionValue::StaticStruct()),
|
||||||
|
ExpectedPin.ContainerType));
|
||||||
|
}
|
||||||
|
|
||||||
|
Disconnect(*MapOutput, *Node, *TargetMap);
|
||||||
|
TestTrue(
|
||||||
|
*FString::Printf(TEXT("%s should reset after a typed disconnect"), *NodeCase.FunctionName.ToString()),
|
||||||
|
HasMapType(*TargetMap, UEdGraphSchema_K2::PC_Wildcard, UEdGraphSchema_K2::PC_Wildcard));
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraph* AppendGraph = MakeGraph();
|
||||||
|
UK2Node_DirectiveUtilMapAppend* AppendNode = NewObject<UK2Node_DirectiveUtilMapAppend>(AppendGraph);
|
||||||
|
AppendGraph->AddNode(AppendNode);
|
||||||
|
AppendNode->AllocateDefaultPins();
|
||||||
|
TestTrue(
|
||||||
|
"Append node should be defined in an uncooked-only module",
|
||||||
|
AppendNode->GetClass()->GetOutermost()->HasAnyPackageFlags(PKG_UncookedOnly));
|
||||||
|
FCompilerResultsLog ModuleValidationLog;
|
||||||
|
FBlueprintEditorUtils::ValidateEditorOnlyNodes(AppendNode, ModuleValidationLog);
|
||||||
|
TestEqual("Append should be valid in a runtime Blueprint", ModuleValidationLog.NumWarnings, 0);
|
||||||
|
TestEqual(
|
||||||
|
"Append should call the runtime append function",
|
||||||
|
AppendNode->GetTargetFunction(),
|
||||||
|
UDirectiveUtilMapFunctionLibrary::StaticClass()->FindFunctionByName(GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMapFunctionLibrary, Map_Append)));
|
||||||
|
|
||||||
|
UEdGraphPin* TargetMap = AppendNode->FindPin(TEXT("TargetMap"));
|
||||||
|
UEdGraphPin* SourceMap = AppendNode->FindPin(TEXT("SourceMap"));
|
||||||
|
TestNotNull("Append should have a TargetMap pin", TargetMap);
|
||||||
|
TestNotNull("Append should have a SourceMap pin", SourceMap);
|
||||||
|
if (!TargetMap || !SourceMap)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestTrue("Append TargetMap should begin as a wildcard map", HasMapType(*TargetMap, UEdGraphSchema_K2::PC_Wildcard, UEdGraphSchema_K2::PC_Wildcard));
|
||||||
|
TestTrue("Append SourceMap should begin as a wildcard map", HasMapType(*SourceMap, UEdGraphSchema_K2::PC_Wildcard, UEdGraphSchema_K2::PC_Wildcard));
|
||||||
|
|
||||||
|
UEdGraphPin* SourceOutput = AddMapOutput(*AppendGraph, TEXT("SourceOutput"), StringIntMap);
|
||||||
|
Connect(*SourceOutput, *AppendNode, *SourceMap);
|
||||||
|
TestTrue("Append TargetMap should resolve from SourceMap", HasMapType(*TargetMap, UEdGraphSchema_K2::PC_String, UEdGraphSchema_K2::PC_Int));
|
||||||
|
TestTrue("Append SourceMap should resolve from SourceMap", HasMapType(*SourceMap, UEdGraphSchema_K2::PC_String, UEdGraphSchema_K2::PC_Int));
|
||||||
|
|
||||||
|
FString DisallowReason;
|
||||||
|
const FEdGraphPinType NameIntMap = MakeMapType(UEdGraphSchema_K2::PC_Name, UEdGraphSchema_K2::PC_Int);
|
||||||
|
UEdGraphPin* MismatchedKeyOutput = AddMapOutput(*AppendGraph, TEXT("MismatchedKeyOutput"), NameIntMap);
|
||||||
|
TestTrue("Append should reject a different key type", AppendNode->IsConnectionDisallowed(TargetMap, MismatchedKeyOutput, DisallowReason));
|
||||||
|
|
||||||
|
const FEdGraphPinType StringStringMap = MakeMapType(UEdGraphSchema_K2::PC_String, UEdGraphSchema_K2::PC_String);
|
||||||
|
UEdGraphPin* MismatchedValueOutput = AddMapOutput(*AppendGraph, TEXT("MismatchedValueOutput"), StringStringMap);
|
||||||
|
TestTrue("Append should reject a different value type", AppendNode->IsConnectionDisallowed(TargetMap, MismatchedValueOutput, DisallowReason));
|
||||||
|
TestFalse("Append should accept matching map types", AppendNode->IsConnectionDisallowed(TargetMap, SourceOutput, DisallowReason));
|
||||||
|
|
||||||
|
Disconnect(*SourceOutput, *AppendNode, *SourceMap);
|
||||||
|
TestTrue("Append TargetMap should reset after disconnect", HasMapType(*TargetMap, UEdGraphSchema_K2::PC_Wildcard, UEdGraphSchema_K2::PC_Wildcard));
|
||||||
|
TestTrue("Append SourceMap should reset after disconnect", HasMapType(*SourceMap, UEdGraphSchema_K2::PC_Wildcard, UEdGraphSchema_K2::PC_Wildcard));
|
||||||
|
|
||||||
|
UEdGraphPin* TypedSourceOutput = AddMapOutput(*AppendGraph, TEXT("TypedSourceOutput"), ObjectStructMap);
|
||||||
|
Connect(*TypedSourceOutput, *AppendNode, *SourceMap);
|
||||||
|
TestTrue("Append TargetMap should preserve subtype objects", HasMapType(*TargetMap, ObjectStructMap));
|
||||||
|
TestTrue("Append SourceMap should preserve subtype objects", HasMapType(*SourceMap, ObjectStructMap));
|
||||||
|
Disconnect(*TypedSourceOutput, *AppendNode, *SourceMap);
|
||||||
|
|
||||||
|
UEdGraph* MigrationGraph = MakeGraph();
|
||||||
|
UBlueprint* MigrationBlueprint = CastChecked<UBlueprint>(MigrationGraph->GetOuter());
|
||||||
|
UK2Node_CallFunction* LegacyAppendNode = AddFunctionNode(
|
||||||
|
*MigrationGraph,
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMapFunctionLibrary, Map_Append));
|
||||||
|
LegacyAppendNode->NodePosX = 320;
|
||||||
|
LegacyAppendNode->NodePosY = 180;
|
||||||
|
UEdGraphPin* LegacyTargetMap = LegacyAppendNode->FindPin(TEXT("TargetMap"));
|
||||||
|
UEdGraphPin* LegacyOverwrite = LegacyAppendNode->FindPin(TEXT("bOverwriteExisting"));
|
||||||
|
TestNotNull("Legacy Append should have a TargetMap pin", LegacyTargetMap);
|
||||||
|
TestNotNull("Legacy Append should have an overwrite pin", LegacyOverwrite);
|
||||||
|
if (!LegacyTargetMap || !LegacyOverwrite)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LegacyOverwrite->DefaultValue = TEXT("false");
|
||||||
|
UEdGraphPin* LegacyMapOutput = AddMapOutput(*MigrationGraph, TEXT("LegacyMapOutput"), StringIntMap);
|
||||||
|
Connect(*LegacyMapOutput, *LegacyAppendNode, *LegacyTargetMap);
|
||||||
|
TestTrue("Legacy Append should be upgraded", DirectiveUtilMapNodeMigration::UpgradeLegacyAppendNodes(*MigrationBlueprint));
|
||||||
|
|
||||||
|
TArray<UK2Node_DirectiveUtilMapAppend*> MigratedAppendNodes;
|
||||||
|
MigrationGraph->GetNodesOfClass(MigratedAppendNodes);
|
||||||
|
TestEqual("Migration should create one Append node", MigratedAppendNodes.Num(), 1);
|
||||||
|
if (MigratedAppendNodes.Num() != 1)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UK2Node_DirectiveUtilMapAppend* MigratedAppendNode = MigratedAppendNodes[0];
|
||||||
|
TestEqual("Migration should preserve the X position", MigratedAppendNode->NodePosX, 320);
|
||||||
|
TestEqual("Migration should preserve the Y position", MigratedAppendNode->NodePosY, 180);
|
||||||
|
TestEqual("Migration should preserve the overwrite default", MigratedAppendNode->FindPinChecked(TEXT("bOverwriteExisting"))->DefaultValue, FString(TEXT("false")));
|
||||||
|
TestTrue("Migrated TargetMap should preserve its connection", HasMapType(*MigratedAppendNode->FindPinChecked(TEXT("TargetMap")), UEdGraphSchema_K2::PC_String, UEdGraphSchema_K2::PC_Int));
|
||||||
|
TestTrue("Migrated SourceMap should resolve from TargetMap", HasMapType(*MigratedAppendNode->FindPinChecked(TEXT("SourceMap")), UEdGraphSchema_K2::PC_String, UEdGraphSchema_K2::PC_Int));
|
||||||
|
|
||||||
|
UEdGraph* FailedMigrationGraph = MakeGraph();
|
||||||
|
UBlueprint* FailedMigrationBlueprint = CastChecked<UBlueprint>(FailedMigrationGraph->GetOuter());
|
||||||
|
UK2Node_CallFunction* IncompatibleLegacyNode = AddFunctionNode(
|
||||||
|
*FailedMigrationGraph,
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMapFunctionLibrary, Map_Append));
|
||||||
|
UEdGraphPin* IncompatibleTargetMap = IncompatibleLegacyNode->FindPinChecked(TEXT("TargetMap"));
|
||||||
|
UEdGraphPin* LegacyOnlyPin = IncompatibleLegacyNode->CreatePin(
|
||||||
|
EGPD_Input,
|
||||||
|
UEdGraphSchema_K2::PC_Int,
|
||||||
|
TEXT("LegacyOnly"));
|
||||||
|
LegacyOnlyPin->ParentPin = IncompatibleTargetMap;
|
||||||
|
IncompatibleTargetMap->SubPins.Add(LegacyOnlyPin);
|
||||||
|
UEdGraphPin* IncompatibleMapOutput = AddMapOutput(*FailedMigrationGraph, TEXT("IncompatibleMapOutput"), StringIntMap);
|
||||||
|
Connect(*IncompatibleMapOutput, *IncompatibleLegacyNode, *IncompatibleTargetMap);
|
||||||
|
AddExpectedMessagePlain(
|
||||||
|
TEXT("BackwardCompatibilityNodeConversion Error 'cannot find pin LegacyOnly"),
|
||||||
|
ELogVerbosity::Warning,
|
||||||
|
EAutomationExpectedMessageFlags::Contains,
|
||||||
|
1);
|
||||||
|
TestFalse(
|
||||||
|
"An incompatible legacy Append node should not be replaced",
|
||||||
|
DirectiveUtilMapNodeMigration::UpgradeLegacyAppendNodes(*FailedMigrationBlueprint));
|
||||||
|
TestTrue(
|
||||||
|
"A failed migration should preserve the legacy connection",
|
||||||
|
IncompatibleMapOutput->LinkedTo.Contains(IncompatibleTargetMap));
|
||||||
|
TestTrue(
|
||||||
|
"A failed migration should preserve the legacy node",
|
||||||
|
FailedMigrationGraph->Nodes.Contains(IncompatibleLegacyNode));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "Libraries/DirectiveUtilMapFunctionLibrary.h"
|
||||||
|
#include "Tests/DirectiveUtilTestObject.h"
|
||||||
|
|
||||||
|
#include "Misc/AutomationTest.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
template <typename PropertyType>
|
||||||
|
PropertyType* GetTestProperty(const FName PropertyName)
|
||||||
|
{
|
||||||
|
return FindFProperty<PropertyType>(UDirectiveUtilTestObject::StaticClass(), PropertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
TSet<int32> MakeIntegerSet(const TArray<int32>& Values)
|
||||||
|
{
|
||||||
|
TSet<int32> Result;
|
||||||
|
for (const int32 Value : Values)
|
||||||
|
{
|
||||||
|
Result.Add(Value);
|
||||||
|
}
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
TSet<FString> MakeStringSet(const TArray<FString>& Values)
|
||||||
|
{
|
||||||
|
TSet<FString> Result;
|
||||||
|
for (const FString& Value : Values)
|
||||||
|
{
|
||||||
|
Result.Add(Value);
|
||||||
|
}
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
TSet<FName> MakeNameSet(const TArray<FName>& Values)
|
||||||
|
{
|
||||||
|
TSet<FName> Result;
|
||||||
|
for (const FName Value : Values)
|
||||||
|
{
|
||||||
|
Result.Add(Value);
|
||||||
|
}
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ValueType>
|
||||||
|
bool HaveSameValues(const TSet<ValueType>& Left, const TSet<ValueType>& Right)
|
||||||
|
{
|
||||||
|
if (Left.Num() != Right.Num())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const ValueType& Value : Left)
|
||||||
|
{
|
||||||
|
if (!Right.Contains(Value))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
|
FDirectiveUtilMapScenarioTest,
|
||||||
|
"DirectiveUtilities.MapScenarios.CardinalityAndTypes",
|
||||||
|
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FDirectiveUtilMapScenarioTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
FMapProperty* IntegerMapProperty = GetTestProperty<FMapProperty>(GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestMap));
|
||||||
|
FArrayProperty* IntegerArrayProperty = GetTestProperty<FArrayProperty>(GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestArray));
|
||||||
|
FMapProperty* StringMapProperty = GetTestProperty<FMapProperty>(GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestStringMap));
|
||||||
|
FMapProperty* StringMapProperty2 = GetTestProperty<FMapProperty>(GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestStringMap2));
|
||||||
|
FArrayProperty* StringArrayProperty = GetTestProperty<FArrayProperty>(GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestStringArray));
|
||||||
|
FMapProperty* StructMapProperty = GetTestProperty<FMapProperty>(GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestStructValueMap));
|
||||||
|
FArrayProperty* NameArrayProperty = GetTestProperty<FArrayProperty>(GET_MEMBER_NAME_CHECKED(UDirectiveUtilTestObject, TestNameArray));
|
||||||
|
if (!TestNotNull("Integer map property should be available", IntegerMapProperty)
|
||||||
|
|| !TestNotNull("Integer array property should be available", IntegerArrayProperty)
|
||||||
|
|| !TestNotNull("String map property should be available", StringMapProperty)
|
||||||
|
|| !TestNotNull("Second string map property should be available", StringMapProperty2)
|
||||||
|
|| !TestNotNull("String array property should be available", StringArrayProperty)
|
||||||
|
|| !TestNotNull("Struct map property should be available", StructMapProperty)
|
||||||
|
|| !TestNotNull("Name array property should be available", NameArrayProperty))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UDirectiveUtilTestObject* TestObject = NewObject<UDirectiveUtilTestObject>();
|
||||||
|
for (const int32 ItemCount : {0, 1, 2, 16, 257, 4096})
|
||||||
|
{
|
||||||
|
TestObject->TestMap.Reset();
|
||||||
|
for (int32 Key = 0; Key < ItemCount; ++Key)
|
||||||
|
{
|
||||||
|
TestObject->TestMap.Add(Key, Key % 7);
|
||||||
|
}
|
||||||
|
const FString Label = FString::Printf(TEXT("integer items=%d"), ItemCount);
|
||||||
|
const int32 QueryValue = 3;
|
||||||
|
TArray<int32> ExpectedKeys;
|
||||||
|
for (int32 Key = 0; Key < ItemCount; ++Key)
|
||||||
|
{
|
||||||
|
if (Key % 7 == QueryValue)
|
||||||
|
{
|
||||||
|
ExpectedKeys.Add(Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TestObject->TestArray.Reset();
|
||||||
|
UDirectiveUtilMapFunctionLibrary::GenericMap_GetKeysByValue(
|
||||||
|
&TestObject->TestMap,
|
||||||
|
IntegerMapProperty,
|
||||||
|
&QueryValue,
|
||||||
|
&TestObject->TestArray,
|
||||||
|
IntegerArrayProperty);
|
||||||
|
TestEqual(Label + TEXT(" GetKeysByValue count"), TestObject->TestArray.Num(), ExpectedKeys.Num());
|
||||||
|
TestTrue(Label + TEXT(" GetKeysByValue values"), HaveSameValues(MakeIntegerSet(TestObject->TestArray), MakeIntegerSet(ExpectedKeys)));
|
||||||
|
TestEqual(
|
||||||
|
Label + TEXT(" HasValue"),
|
||||||
|
UDirectiveUtilMapFunctionLibrary::GenericMap_HasValue(&TestObject->TestMap, IntegerMapProperty, &QueryValue),
|
||||||
|
!ExpectedKeys.IsEmpty());
|
||||||
|
|
||||||
|
const int32 ExistingKey = ItemCount > 0 ? ItemCount / 2 : 0;
|
||||||
|
int32 FoundValue = -1;
|
||||||
|
UDirectiveUtilMapFunctionLibrary::GenericMap_FindOrAdd(
|
||||||
|
&TestObject->TestMap,
|
||||||
|
IntegerMapProperty,
|
||||||
|
&ExistingKey,
|
||||||
|
&FoundValue);
|
||||||
|
TestEqual(Label + TEXT(" FindOrAdd value"), FoundValue, ItemCount > 0 ? ExistingKey % 7 : 0);
|
||||||
|
|
||||||
|
UDirectiveUtilMapFunctionLibrary::GenericMap_ClearValues(&TestObject->TestMap, IntegerMapProperty);
|
||||||
|
TestEqual(Label + TEXT(" ClearValues count"), TestObject->TestMap.Num(), FMath::Max(ItemCount, 1));
|
||||||
|
for (const TPair<int32, int32>& Pair : TestObject->TestMap)
|
||||||
|
{
|
||||||
|
TestEqual(Label + TEXT(" ClearValues value"), Pair.Value, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TestObject->TestArray.Reset();
|
||||||
|
for (int32 Key = 0; Key < ItemCount; Key += 3)
|
||||||
|
{
|
||||||
|
TestObject->TestArray.Add(Key);
|
||||||
|
}
|
||||||
|
const int32 ExpectedRemoved = TestObject->TestArray.Num();
|
||||||
|
const int32 Removed = UDirectiveUtilMapFunctionLibrary::GenericMap_RemoveKeys(
|
||||||
|
&TestObject->TestMap,
|
||||||
|
IntegerMapProperty,
|
||||||
|
&TestObject->TestArray,
|
||||||
|
IntegerArrayProperty);
|
||||||
|
TestEqual(Label + TEXT(" RemoveKeys count"), Removed, ExpectedRemoved);
|
||||||
|
TestEqual(Label + TEXT(" RemoveKeys remaining"), TestObject->TestMap.Num(), FMath::Max(ItemCount, 1) - ExpectedRemoved);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const int32 ItemCount : {0, 1, 16, 257})
|
||||||
|
{
|
||||||
|
TestObject->TestStringMap.Reset();
|
||||||
|
TestObject->TestStringMap2.Reset();
|
||||||
|
for (int32 Index = 0; Index < ItemCount; ++Index)
|
||||||
|
{
|
||||||
|
const FString Key = FString::Printf(TEXT("Key%d"), Index);
|
||||||
|
TestObject->TestStringMap.Add(Key, FString::Printf(TEXT("Original%d"), Index));
|
||||||
|
TestObject->TestStringMap2.Add(Key, FString::Printf(TEXT("Replacement%d"), Index));
|
||||||
|
}
|
||||||
|
TestObject->TestStringMap2.Add(TEXT("Added"), TEXT("AddedValue"));
|
||||||
|
const FString Label = FString::Printf(TEXT("string items=%d"), ItemCount);
|
||||||
|
|
||||||
|
UDirectiveUtilMapFunctionLibrary::GenericMap_Append(
|
||||||
|
&TestObject->TestStringMap,
|
||||||
|
StringMapProperty,
|
||||||
|
&TestObject->TestStringMap2,
|
||||||
|
StringMapProperty2,
|
||||||
|
false);
|
||||||
|
TestEqual(Label + TEXT(" Append count"), TestObject->TestStringMap.Num(), ItemCount + 1);
|
||||||
|
TestEqual(Label + TEXT(" Append new value"), TestObject->TestStringMap.FindRef(TEXT("Added")), FString(TEXT("AddedValue")));
|
||||||
|
if (ItemCount > 0)
|
||||||
|
{
|
||||||
|
TestEqual(Label + TEXT(" Append preserve"), TestObject->TestStringMap.FindRef(TEXT("Key0")), FString(TEXT("Original0")));
|
||||||
|
}
|
||||||
|
|
||||||
|
UDirectiveUtilMapFunctionLibrary::GenericMap_Append(
|
||||||
|
&TestObject->TestStringMap,
|
||||||
|
StringMapProperty,
|
||||||
|
&TestObject->TestStringMap2,
|
||||||
|
StringMapProperty2,
|
||||||
|
true);
|
||||||
|
if (ItemCount > 0)
|
||||||
|
{
|
||||||
|
TestEqual(Label + TEXT(" Append overwrite"), TestObject->TestStringMap.FindRef(TEXT("Key0")), FString(TEXT("Replacement0")));
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 CountBeforeSelfAppend = TestObject->TestStringMap.Num();
|
||||||
|
UDirectiveUtilMapFunctionLibrary::GenericMap_Append(
|
||||||
|
&TestObject->TestStringMap,
|
||||||
|
StringMapProperty,
|
||||||
|
&TestObject->TestStringMap,
|
||||||
|
StringMapProperty,
|
||||||
|
true);
|
||||||
|
TestEqual(Label + TEXT(" self append"), TestObject->TestStringMap.Num(), CountBeforeSelfAppend);
|
||||||
|
|
||||||
|
const FString QueryValue = ItemCount > 0 ? TEXT("Replacement0") : TEXT("missing");
|
||||||
|
TestObject->TestStringArray.Reset();
|
||||||
|
UDirectiveUtilMapFunctionLibrary::GenericMap_GetKeysByValue(
|
||||||
|
&TestObject->TestStringMap,
|
||||||
|
StringMapProperty,
|
||||||
|
&QueryValue,
|
||||||
|
&TestObject->TestStringArray,
|
||||||
|
StringArrayProperty);
|
||||||
|
const TArray<FString> ExpectedKeys = ItemCount > 0 ? TArray<FString>({TEXT("Key0")}) : TArray<FString>();
|
||||||
|
TestTrue(Label + TEXT(" managed GetKeysByValue"), HaveSameValues(MakeStringSet(TestObject->TestStringArray), MakeStringSet(ExpectedKeys)));
|
||||||
|
|
||||||
|
UDirectiveUtilMapFunctionLibrary::GenericMap_ClearValues(&TestObject->TestStringMap, StringMapProperty);
|
||||||
|
for (const TPair<FString, FString>& Pair : TestObject->TestStringMap)
|
||||||
|
{
|
||||||
|
TestTrue(Label + TEXT(" managed ClearValues"), Pair.Value.IsEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FDirectiveUtilCollisionValue FirstStructValue;
|
||||||
|
FirstStructValue.Value = 1;
|
||||||
|
FDirectiveUtilCollisionValue SecondStructValue;
|
||||||
|
SecondStructValue.Value = 2;
|
||||||
|
TestObject->TestStructValueMap = {
|
||||||
|
{TEXT("Alpha"), FirstStructValue},
|
||||||
|
{TEXT("Beta"), SecondStructValue},
|
||||||
|
{TEXT("Gamma"), FirstStructValue}
|
||||||
|
};
|
||||||
|
const FDirectiveUtilCollisionValue StructQuery = FirstStructValue;
|
||||||
|
TestObject->TestNameArray.Reset();
|
||||||
|
UDirectiveUtilMapFunctionLibrary::GenericMap_GetKeysByValue(
|
||||||
|
&TestObject->TestStructValueMap,
|
||||||
|
StructMapProperty,
|
||||||
|
&StructQuery,
|
||||||
|
&TestObject->TestNameArray,
|
||||||
|
NameArrayProperty);
|
||||||
|
TestTrue(
|
||||||
|
"Struct GetKeysByValue",
|
||||||
|
HaveSameValues(
|
||||||
|
MakeNameSet(TestObject->TestNameArray),
|
||||||
|
MakeNameSet(TArray<FName>({TEXT("Alpha"), TEXT("Gamma")}))));
|
||||||
|
TestTrue(
|
||||||
|
"Struct HasValue",
|
||||||
|
UDirectiveUtilMapFunctionLibrary::GenericMap_HasValue(
|
||||||
|
&TestObject->TestStructValueMap,
|
||||||
|
StructMapProperty,
|
||||||
|
&StructQuery));
|
||||||
|
UDirectiveUtilMapFunctionLibrary::GenericMap_ClearValues(&TestObject->TestStructValueMap, StructMapProperty);
|
||||||
|
for (const TPair<FName, FDirectiveUtilCollisionValue>& Pair : TestObject->TestStructValueMap)
|
||||||
|
{
|
||||||
|
TestEqual("Struct ClearValues", Pair.Value.Value, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,540 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "Libraries/DirectiveUtilMathFunctionLibrary.h"
|
||||||
|
#include "Async/ParallelFor.h"
|
||||||
|
#include "Misc/AutomationTest.h"
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
|
FDirectiveUtilMathExtendedFunctionLibraryTest,
|
||||||
|
"DirectiveUtilities.Math.ExtendedFunctionLibrary",
|
||||||
|
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FDirectiveUtilMathExtendedFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
float FloatResult = 0.0f;
|
||||||
|
float Strength = 0.0f;
|
||||||
|
TestTrue("Angle array average accepts values across the degree seam",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetAngleArrayAverage({350.0f, 10.0f}, FloatResult, Strength));
|
||||||
|
TestTrue("Angle array average crosses the degree seam",
|
||||||
|
FMath::IsNearlyZero(FloatResult, 1.e-4f));
|
||||||
|
TestTrue("Angle array average reports concentration",
|
||||||
|
FMath::IsNearlyEqual(Strength, FMath::Cos(FMath::DegreesToRadians(10.0f)), 1.e-4f));
|
||||||
|
TestFalse("Angle array average rejects an undefined antipodal mean",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetAngleArrayAverage({0.0f, 180.0f}, FloatResult, Strength));
|
||||||
|
TestFalse("Angle array average rejects non-finite values",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetAngleArrayAverage(
|
||||||
|
{0.0f, std::numeric_limits<float>::quiet_NaN()}, FloatResult, Strength));
|
||||||
|
|
||||||
|
TestTrue("Weighted float average accepts aligned arrays",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetWeightedFloatArrayAverage(
|
||||||
|
{10.0f, 20.0f}, {1.0f, 3.0f}, FloatResult));
|
||||||
|
TestTrue("Weighted float average applies weights",
|
||||||
|
FMath::IsNearlyEqual(FloatResult, 17.5f, 1.e-4f));
|
||||||
|
TestTrue("Weighted float average ignores negative weights",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetWeightedFloatArrayAverage(
|
||||||
|
{10.0f, 20.0f}, {-1.0f, 2.0f}, FloatResult)
|
||||||
|
&& FMath::IsNearlyEqual(FloatResult, 20.0f, 1.e-4f));
|
||||||
|
TestFalse("Weighted float average rejects mismatched arrays",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetWeightedFloatArrayAverage(
|
||||||
|
{10.0f}, {1.0f, 2.0f}, FloatResult));
|
||||||
|
|
||||||
|
FVector VectorResult = FVector::ZeroVector;
|
||||||
|
TestTrue("Weighted vector average accepts aligned arrays",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetWeightedVectorArrayAverage(
|
||||||
|
{FVector::ForwardVector, FVector::RightVector}, {1.0f, 1.0f}, VectorResult));
|
||||||
|
TestTrue("Weighted vector average applies weights",
|
||||||
|
VectorResult.Equals(FVector(0.5, 0.5, 0.0), 1.e-6));
|
||||||
|
TestTrue("Weighted vector average preserves large finite values",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetWeightedVectorArrayAverage(
|
||||||
|
{FVector(1.e300, 0.0, 0.0)}, {1.e20f}, VectorResult)
|
||||||
|
&& FMath::IsNearlyEqual(VectorResult.X / 1.e300, 1.0, 1.e-12));
|
||||||
|
TestFalse("Weighted vector average rejects non-finite values",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetWeightedVectorArrayAverage(
|
||||||
|
{FVector::ForwardVector, FVector(std::numeric_limits<double>::infinity(), 0.0, 0.0)},
|
||||||
|
{1.0f, 1.0f}, VectorResult));
|
||||||
|
|
||||||
|
TArray<float> FloatArrayResult;
|
||||||
|
TestTrue("Float array normalization accepts finite values",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::NormalizeFloatArrayToRange(
|
||||||
|
{2.0f, 4.0f, 6.0f}, -1.0f, 1.0f, FloatArrayResult));
|
||||||
|
TestTrue("Float array normalization maps the full range",
|
||||||
|
FloatArrayResult.Num() == 3
|
||||||
|
&& FMath::IsNearlyEqual(FloatArrayResult[0], -1.0f)
|
||||||
|
&& FMath::IsNearlyZero(FloatArrayResult[1])
|
||||||
|
&& FMath::IsNearlyEqual(FloatArrayResult[2], 1.0f));
|
||||||
|
TestTrue("Float array normalization accepts reversed output bounds",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::NormalizeFloatArrayToRange(
|
||||||
|
{2.0f, 4.0f, 6.0f}, 1.0f, -1.0f, FloatArrayResult)
|
||||||
|
&& FMath::IsNearlyEqual(FloatArrayResult[0], 1.0f)
|
||||||
|
&& FMath::IsNearlyEqual(FloatArrayResult[2], -1.0f));
|
||||||
|
TestTrue("Float array normalization maps a constant array to the output minimum",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::NormalizeFloatArrayToRange(
|
||||||
|
{4.0f, 4.0f}, 5.0f, 10.0f, FloatArrayResult)
|
||||||
|
&& FloatArrayResult == TArray<float>({5.0f, 5.0f}));
|
||||||
|
TestFalse("Float array normalization rejects an empty array",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::NormalizeFloatArrayToRange(
|
||||||
|
{}, 0.0f, 1.0f, FloatArrayResult));
|
||||||
|
TArray<float> InPlaceValues = {2.0f, 4.0f, 6.0f};
|
||||||
|
TestTrue("Float array normalization supports the same input and output array",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::NormalizeFloatArrayToRange(
|
||||||
|
InPlaceValues, -1.0f, 1.0f, InPlaceValues)
|
||||||
|
&& InPlaceValues == TArray<float>({-1.0f, 0.0f, 1.0f}));
|
||||||
|
|
||||||
|
TestTrue("Weight normalization accepts positive weights",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::NormalizeWeights({1.0f, 3.0f}, FloatArrayResult));
|
||||||
|
TestTrue("Weight normalization sums to one",
|
||||||
|
FloatArrayResult.Num() == 2
|
||||||
|
&& FMath::IsNearlyEqual(FloatArrayResult[0], 0.25f)
|
||||||
|
&& FMath::IsNearlyEqual(FloatArrayResult[1], 0.75f));
|
||||||
|
TestTrue("Weight normalization clears negative weights",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::NormalizeWeights({-2.0f, 2.0f}, FloatArrayResult)
|
||||||
|
&& FMath::IsNearlyZero(FloatArrayResult[0])
|
||||||
|
&& FMath::IsNearlyEqual(FloatArrayResult[1], 1.0f));
|
||||||
|
TestFalse("Weight normalization rejects all-zero weights",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::NormalizeWeights({0.0f, -1.0f}, FloatArrayResult));
|
||||||
|
TArray<float> InPlaceWeights = {1.0f, 3.0f};
|
||||||
|
TestTrue("Weight normalization supports the same input and output array",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::NormalizeWeights(InPlaceWeights, InPlaceWeights)
|
||||||
|
&& InPlaceWeights == TArray<float>({0.25f, 0.75f}));
|
||||||
|
|
||||||
|
TestTrue("Float array percentile accepts finite values",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile(
|
||||||
|
{4.0f, 1.0f, 3.0f, 2.0f}, 25.0f, FloatResult));
|
||||||
|
TestTrue("Float array percentile interpolates adjacent values",
|
||||||
|
FMath::IsNearlyEqual(FloatResult, 1.75f, 1.e-4f));
|
||||||
|
TestTrue("Float array percentile clamps above one hundred",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile(
|
||||||
|
{1.0f, 4.0f}, 125.0f, FloatResult)
|
||||||
|
&& FMath::IsNearlyEqual(FloatResult, 4.0f));
|
||||||
|
TestTrue("Float array percentile uses the Type 7 sample position",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile(
|
||||||
|
{10.0f, 1.0f, 8.0f, 2.0f, 7.0f, 3.0f, 6.0f, 4.0f, 9.0f, 5.0f}, 40.0f, FloatResult)
|
||||||
|
&& FMath::IsNearlyEqual(FloatResult, 4.6f, 1.e-4f));
|
||||||
|
TestFalse("Float array percentile rejects non-finite values",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile(
|
||||||
|
{1.0f, std::numeric_limits<float>::infinity()}, 50.0f, FloatResult));
|
||||||
|
|
||||||
|
TestTrue("Root mean square accepts finite values",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetFloatArrayRootMeanSquare({3.0f, 4.0f}, FloatResult));
|
||||||
|
TestTrue("Root mean square uses the arithmetic mean of squares",
|
||||||
|
FMath::IsNearlyEqual(FloatResult, FMath::Sqrt(12.5f), 1.e-4f));
|
||||||
|
TestFalse("Root mean square rejects an empty array",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetFloatArrayRootMeanSquare({}, FloatResult));
|
||||||
|
|
||||||
|
TestTrue("Smooth Step clamps below its range",
|
||||||
|
FMath::IsNearlyZero(UDirectiveUtilMathFunctionLibrary::SmoothStep(-1.0f, 0.0f, 1.0f)));
|
||||||
|
TestTrue("Smooth Step reaches its midpoint",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SmoothStep(0.5f, 0.0f, 1.0f), 0.5f));
|
||||||
|
TestTrue("Smooth Step accepts reversed bounds",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SmoothStep(0.25f, 1.0f, 0.0f), 0.15625f));
|
||||||
|
TestTrue("Smooth Step treats equal bounds as a step",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SmoothStep(2.0f, 2.0f, 2.0f), 1.0f));
|
||||||
|
TestTrue("Smoother Step reaches its midpoint",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SmootherStep(0.5f, 0.0f, 1.0f), 0.5f));
|
||||||
|
TestTrue("Smoother Step has quintic shaping",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SmootherStep(0.25f, 0.0f, 1.0f), 0.103515625f));
|
||||||
|
|
||||||
|
TestTrue("Range Falloff applies linear attenuation",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RangeFalloff(5.0f, 0.0f, 10.0f), 0.5f));
|
||||||
|
TestTrue("Range Falloff applies its exponent",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RangeFalloff(5.0f, 0.0f, 10.0f, 2.0f), 0.25f));
|
||||||
|
TestTrue("Range Falloff remains one inside the inner radius",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RangeFalloff(2.0f, 3.0f, 10.0f), 1.0f));
|
||||||
|
TestTrue("Range Falloff reaches zero at the outer radius",
|
||||||
|
FMath::IsNearlyZero(UDirectiveUtilMathFunctionLibrary::RangeFalloff(10.0f, 3.0f, 10.0f)));
|
||||||
|
|
||||||
|
double Distance = 0.0;
|
||||||
|
TestTrue("Direction and distance accepts distinct points",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetDirectionAndDistance(
|
||||||
|
FVector::ZeroVector, FVector(3.0, 4.0, 0.0), VectorResult, Distance));
|
||||||
|
TestTrue("Direction and distance returns a unit direction",
|
||||||
|
VectorResult.Equals(FVector(0.6, 0.8, 0.0), 1.e-6));
|
||||||
|
TestTrue("Direction and distance returns the length",
|
||||||
|
FMath::IsNearlyEqual(Distance, 5.0));
|
||||||
|
TestFalse("Direction and distance rejects equal points",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetDirectionAndDistance(
|
||||||
|
FVector::ZeroVector, FVector::ZeroVector, VectorResult, Distance));
|
||||||
|
TestTrue("Direction and distance handles large finite coordinates",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetDirectionAndDistance(
|
||||||
|
FVector::ZeroVector, FVector(1.e200, 0.0, 0.0), VectorResult, Distance)
|
||||||
|
&& VectorResult.Equals(FVector::ForwardVector, 1.e-12)
|
||||||
|
&& FMath::IsNearlyEqual(Distance / 1.e200, 1.0, 1.e-12));
|
||||||
|
|
||||||
|
TestTrue("Signed angle handles large finite vectors",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors(
|
||||||
|
FVector(1.e200, 0.0, 0.0), FVector(0.0, 1.e200, 0.0), FVector::UpVector), 90.0f, 1.e-4f));
|
||||||
|
TestTrue("Direction Within Cone handles large finite vectors",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone(
|
||||||
|
FVector(1.e200, 0.0, 0.0), FVector(1.e200, 0.0, 0.0), 0.0f));
|
||||||
|
TestTrue("Direction Within Cone includes an identical non-axis direction at zero width",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone(
|
||||||
|
FVector(3.e200, 2.e200, 1.e200), FVector(3.e200, 2.e200, 1.e200), 0.0f));
|
||||||
|
TestFalse("Direction Within Cone excludes a measurable angle from a zero-width cone",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone(
|
||||||
|
FVector(FMath::Cos(FMath::DegreesToRadians(0.005)), FMath::Sin(FMath::DegreesToRadians(0.005)), 0.0),
|
||||||
|
FVector::ForwardVector, 0.0f));
|
||||||
|
|
||||||
|
const FVector2D RotatedPoint = UDirectiveUtilMathFunctionLibrary::RotatePointAroundPivot2D(
|
||||||
|
FVector2D(2.0, 1.0), FVector2D(1.0, 1.0), 90.0f);
|
||||||
|
TestTrue("Rotate Point Around Pivot 2D preserves the pivot offset",
|
||||||
|
RotatedPoint.Equals(FVector2D(1.0, 2.0), 1.e-6));
|
||||||
|
TestTrue("Rotate Point Around Pivot 2D rejects non-finite input",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::RotatePointAroundPivot2D(
|
||||||
|
FVector2D(std::numeric_limits<double>::infinity(), 0.0), FVector2D::ZeroVector, 90.0f).IsZero());
|
||||||
|
|
||||||
|
TestTrue("Signed Distance To Plane is positive in front of the plane",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedDistanceToPlane(
|
||||||
|
FVector(0.0, 0.0, 5.0), FVector(0.0, 0.0, 2.0), FVector::UpVector), 3.0));
|
||||||
|
TestTrue("Signed Distance To Plane follows the normal direction",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedDistanceToPlane(
|
||||||
|
FVector(0.0, 0.0, 5.0), FVector(0.0, 0.0, 2.0), -FVector::UpVector), -3.0));
|
||||||
|
TestTrue("Signed Distance To Plane rejects a zero normal",
|
||||||
|
FMath::IsNearlyZero(UDirectiveUtilMathFunctionLibrary::SignedDistanceToPlane(
|
||||||
|
FVector::UpVector, FVector::ZeroVector, FVector::ZeroVector)));
|
||||||
|
TestTrue("Signed Distance To Plane handles a large finite normal",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedDistanceToPlane(
|
||||||
|
FVector::ForwardVector, FVector::ZeroVector, FVector(1.e200, 0.0, 0.0)), 1.0, 1.e-12));
|
||||||
|
|
||||||
|
TestTrue("Point Within Cone includes a point inside the cone",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::IsPointWithinCone(
|
||||||
|
FVector(10.0, 0.0, 0.0), FVector::ZeroVector, FVector::ForwardVector, 10.0f, 20.0));
|
||||||
|
TestFalse("Point Within Cone excludes a point outside the cone",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::IsPointWithinCone(
|
||||||
|
FVector(0.0, 10.0, 0.0), FVector::ZeroVector, FVector::ForwardVector, 10.0f, 20.0));
|
||||||
|
TestFalse("Point Within Cone applies the maximum distance",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::IsPointWithinCone(
|
||||||
|
FVector(10.0, 0.0, 0.0), FVector::ZeroVector, FVector::ForwardVector, 10.0f, 5.0));
|
||||||
|
TestTrue("Point Within Cone treats zero maximum distance as unlimited",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::IsPointWithinCone(
|
||||||
|
FVector(10.0, 0.0, 0.0), FVector::ZeroVector, FVector::ForwardVector, 10.0f));
|
||||||
|
TestFalse("Point Within Cone applies maximum distance to large finite coordinates",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::IsPointWithinCone(
|
||||||
|
FVector(1.5e200, 0.0, 0.0), FVector::ZeroVector, FVector::ForwardVector, 180.0f, 1.e200));
|
||||||
|
|
||||||
|
bool bCircleSamplesValid = true;
|
||||||
|
bool bAnnulusSamplesValid = true;
|
||||||
|
bool bSphereSamplesValid = true;
|
||||||
|
for (int32 Index = 0; Index < 100; ++Index)
|
||||||
|
{
|
||||||
|
const FVector2D CirclePoint = UDirectiveUtilMathFunctionLibrary::RandomPointInCircle(5.0f);
|
||||||
|
const FVector2D AnnulusPoint = UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulus(2.0f, 5.0f);
|
||||||
|
const FVector SpherePoint = UDirectiveUtilMathFunctionLibrary::RandomPointInSphere(5.0f);
|
||||||
|
bCircleSamplesValid &= CirclePoint.Size() <= 5.0 + 1.e-6;
|
||||||
|
bAnnulusSamplesValid &= AnnulusPoint.Size() >= 2.0 - 1.e-6 && AnnulusPoint.Size() <= 5.0 + 1.e-6;
|
||||||
|
bSphereSamplesValid &= SpherePoint.Size() <= 5.0 + 1.e-6;
|
||||||
|
}
|
||||||
|
TestTrue("Random Point In Circle stays within its radius", bCircleSamplesValid);
|
||||||
|
TestTrue("Random Point In Annulus stays between its radii", bAnnulusSamplesValid);
|
||||||
|
TestTrue("Random Point In Sphere stays within its radius", bSphereSamplesValid);
|
||||||
|
|
||||||
|
FRandomStream FirstStream(12345);
|
||||||
|
FRandomStream SecondStream(12345);
|
||||||
|
TestTrue("Random Point In Circle stream variant is deterministic",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream(FirstStream, 5.0f).Equals(
|
||||||
|
UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream(SecondStream, 5.0f), 1.e-9));
|
||||||
|
TestTrue("Random Point In Annulus stream variant is deterministic",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream(FirstStream, 2.0f, 5.0f).Equals(
|
||||||
|
UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream(SecondStream, 2.0f, 5.0f), 1.e-9));
|
||||||
|
TestTrue("Random Point In Sphere stream variant is deterministic",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream(FirstStream, 5.0f).Equals(
|
||||||
|
UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream(SecondStream, 5.0f), 1.e-9));
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
const TArray<FName> StreamRandomFunctions = {
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GetRandomIndexFromWeightsFromStream),
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, RandomPointInCircleFromStream),
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, RandomPointInAnnulusFromStream),
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, RandomPointInSphereFromStream)
|
||||||
|
};
|
||||||
|
for (const FName FunctionName : StreamRandomFunctions)
|
||||||
|
{
|
||||||
|
const UFunction* Function = UDirectiveUtilMathFunctionLibrary::StaticClass()->FindFunctionByName(FunctionName);
|
||||||
|
TestTrue(*FString::Printf(TEXT("%s should be exposed to Blueprint"), *FunctionName.ToString()), Function != nullptr);
|
||||||
|
if (Function)
|
||||||
|
{
|
||||||
|
TestFalse(
|
||||||
|
*FString::Printf(TEXT("%s should not advertise inert Blueprint thread safety"), *FunctionName.ToString()),
|
||||||
|
Function->HasMetaData(TEXT("BlueprintThreadSafe")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
TArray<FVector> ParallelRandomResults;
|
||||||
|
ParallelRandomResults.SetNumUninitialized(64);
|
||||||
|
ParallelFor(ParallelRandomResults.Num(), [&ParallelRandomResults](const int32 TaskIndex)
|
||||||
|
{
|
||||||
|
FRandomStream Stream(99173);
|
||||||
|
const FVector2D Circle = UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream(Stream, 5.0f);
|
||||||
|
const FVector2D Annulus = UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream(Stream, 2.0f, 5.0f);
|
||||||
|
ParallelRandomResults[TaskIndex] = UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream(Stream, 5.0f)
|
||||||
|
+ FVector(Circle.X, Circle.Y, Annulus.X + Annulus.Y);
|
||||||
|
});
|
||||||
|
bool bParallelRandomResultsMatch = true;
|
||||||
|
for (int32 Index = 1; Index < ParallelRandomResults.Num(); ++Index)
|
||||||
|
{
|
||||||
|
bParallelRandomResultsMatch &= ParallelRandomResults[Index].Equals(ParallelRandomResults[0], 1.e-12);
|
||||||
|
}
|
||||||
|
TestTrue("Seeded random nodes should remain deterministic across worker tasks", bParallelRandomResultsMatch);
|
||||||
|
|
||||||
|
FRandomStream ExpectedCircleStream(24680);
|
||||||
|
const double ExpectedCircleAngle = static_cast<double>(ExpectedCircleStream.FRand()) * UE_TWO_PI;
|
||||||
|
const double ExpectedCircleRadius = FMath::Sqrt(static_cast<double>(ExpectedCircleStream.FRand())) * 5.0;
|
||||||
|
const FVector2D ExpectedCirclePoint(
|
||||||
|
FMath::Cos(ExpectedCircleAngle) * ExpectedCircleRadius,
|
||||||
|
FMath::Sin(ExpectedCircleAngle) * ExpectedCircleRadius);
|
||||||
|
FRandomStream ActualCircleStream(24680);
|
||||||
|
const FVector2D ActualCirclePoint =
|
||||||
|
UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream(ActualCircleStream, 5.0f);
|
||||||
|
TestTrue("Random Point In Circle consumes angle before radius",
|
||||||
|
ActualCirclePoint.Equals(ExpectedCirclePoint, 1.e-6));
|
||||||
|
TestEqual("Random Point In Circle consumes two stream samples",
|
||||||
|
ActualCircleStream.GetCurrentSeed(), ExpectedCircleStream.GetCurrentSeed());
|
||||||
|
|
||||||
|
FRandomStream ExpectedAnnulusStream(13579);
|
||||||
|
const double ExpectedAnnulusAngle = static_cast<double>(ExpectedAnnulusStream.FRand()) * UE_TWO_PI;
|
||||||
|
const double ExpectedAnnulusRadius = FMath::Sqrt(FMath::Lerp(
|
||||||
|
4.0, 25.0, static_cast<double>(ExpectedAnnulusStream.FRand())));
|
||||||
|
const FVector2D ExpectedAnnulusPoint(
|
||||||
|
FMath::Cos(ExpectedAnnulusAngle) * ExpectedAnnulusRadius,
|
||||||
|
FMath::Sin(ExpectedAnnulusAngle) * ExpectedAnnulusRadius);
|
||||||
|
FRandomStream ActualAnnulusStream(13579);
|
||||||
|
const FVector2D ActualAnnulusPoint =
|
||||||
|
UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream(ActualAnnulusStream, 2.0f, 5.0f);
|
||||||
|
TestTrue("Random Point In Annulus consumes angle before radius",
|
||||||
|
ActualAnnulusPoint.Equals(ExpectedAnnulusPoint, 1.e-6));
|
||||||
|
TestEqual("Random Point In Annulus consumes two stream samples",
|
||||||
|
ActualAnnulusStream.GetCurrentSeed(), ExpectedAnnulusStream.GetCurrentSeed());
|
||||||
|
|
||||||
|
FRandomStream ExpectedSphereStream(97531);
|
||||||
|
FVector ExpectedSpherePoint;
|
||||||
|
double ExpectedSphereSizeSquared;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
const double X = static_cast<double>(ExpectedSphereStream.FRand()) * 2.0 - 1.0;
|
||||||
|
const double Y = static_cast<double>(ExpectedSphereStream.FRand()) * 2.0 - 1.0;
|
||||||
|
const double Z = static_cast<double>(ExpectedSphereStream.FRand()) * 2.0 - 1.0;
|
||||||
|
ExpectedSpherePoint = FVector(X, Y, Z);
|
||||||
|
ExpectedSphereSizeSquared = ExpectedSpherePoint.SizeSquared();
|
||||||
|
}
|
||||||
|
while (ExpectedSphereSizeSquared > 1.0);
|
||||||
|
ExpectedSpherePoint *= 5.0;
|
||||||
|
FRandomStream ActualSphereStream(97531);
|
||||||
|
TestTrue("Random Point In Sphere consumes coordinates in XYZ order",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream(ActualSphereStream, 5.0f).Equals(
|
||||||
|
ExpectedSpherePoint, 1.e-12)
|
||||||
|
&& ActualSphereStream.GetCurrentSeed() == ExpectedSphereStream.GetCurrentSeed());
|
||||||
|
|
||||||
|
FRandomStream UnchangedStream(86420);
|
||||||
|
const int32 UnchangedSeed = UnchangedStream.GetCurrentSeed();
|
||||||
|
UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream(UnchangedStream, 0.0f);
|
||||||
|
UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream(UnchangedStream, 0.0f, 0.0f);
|
||||||
|
UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream(
|
||||||
|
UnchangedStream, std::numeric_limits<float>::infinity());
|
||||||
|
TestEqual("Invalid and zero radii do not advance random streams",
|
||||||
|
UnchangedStream.GetCurrentSeed(), UnchangedSeed);
|
||||||
|
|
||||||
|
FRandomStream DistributionStream(112358);
|
||||||
|
double CircleDistributionMean = 0.0;
|
||||||
|
double AnnulusDistributionMean = 0.0;
|
||||||
|
double SphereDistributionMean = 0.0;
|
||||||
|
constexpr int32 DistributionSampleCount = 10000;
|
||||||
|
for (int32 Index = 0; Index < DistributionSampleCount; ++Index)
|
||||||
|
{
|
||||||
|
const FVector2D CirclePoint = UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream(
|
||||||
|
DistributionStream, 5.0f);
|
||||||
|
const FVector2D AnnulusPoint = UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream(
|
||||||
|
DistributionStream, 2.0f, 5.0f);
|
||||||
|
const FVector SpherePoint = UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream(
|
||||||
|
DistributionStream, 5.0f);
|
||||||
|
CircleDistributionMean += CirclePoint.SizeSquared() / 25.0;
|
||||||
|
AnnulusDistributionMean += (AnnulusPoint.SizeSquared() - 4.0) / 21.0;
|
||||||
|
SphereDistributionMean += FMath::Pow(SpherePoint.Size() / 5.0, 3.0);
|
||||||
|
}
|
||||||
|
CircleDistributionMean /= DistributionSampleCount;
|
||||||
|
AnnulusDistributionMean /= DistributionSampleCount;
|
||||||
|
SphereDistributionMean /= DistributionSampleCount;
|
||||||
|
TestTrue("Random Point In Circle is uniform by area",
|
||||||
|
FMath::IsNearlyEqual(CircleDistributionMean, 0.5, 0.02));
|
||||||
|
TestTrue("Random Point In Annulus is uniform by area",
|
||||||
|
FMath::IsNearlyEqual(AnnulusDistributionMean, 0.5, 0.02));
|
||||||
|
TestTrue("Random Point In Sphere is uniform by volume",
|
||||||
|
FMath::IsNearlyEqual(SphereDistributionMean, 0.5, 0.02));
|
||||||
|
TestTrue("Random point functions reject non-finite radii",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::RandomPointInCircle(
|
||||||
|
std::numeric_limits<float>::quiet_NaN()).IsZero()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulus(
|
||||||
|
0.0f, std::numeric_limits<float>::infinity()).IsZero()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::RandomPointInSphere(
|
||||||
|
std::numeric_limits<float>::infinity()).IsZero());
|
||||||
|
|
||||||
|
TestTrue("Angle array average accepts equivalent angles across multiple turns",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetAngleArrayAverage({730.0f, -710.0f}, FloatResult, Strength)
|
||||||
|
&& FMath::IsNearlyEqual(FloatResult, 10.0f, 1.e-4f)
|
||||||
|
&& FMath::IsNearlyEqual(Strength, 1.0f, 1.e-4f));
|
||||||
|
FloatResult = 123.0f;
|
||||||
|
Strength = 123.0f;
|
||||||
|
TestFalse("Angle array average rejects an empty array",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetAngleArrayAverage({}, FloatResult, Strength));
|
||||||
|
TestTrue("A rejected angle average resets both outputs",
|
||||||
|
FMath::IsNearlyZero(FloatResult) && FMath::IsNearlyZero(Strength));
|
||||||
|
|
||||||
|
FloatResult = 123.0f;
|
||||||
|
TestTrue("Weighted float average ignores unusable weights",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetWeightedFloatArrayAverage(
|
||||||
|
{10.0f, 20.0f, 30.0f, 40.0f},
|
||||||
|
{std::numeric_limits<float>::quiet_NaN(), -1.0f, std::numeric_limits<float>::infinity(), 2.0f},
|
||||||
|
FloatResult)
|
||||||
|
&& FMath::IsNearlyEqual(FloatResult, 40.0f));
|
||||||
|
FloatResult = 123.0f;
|
||||||
|
TestFalse("Weighted float average rejects arrays without a usable weight",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetWeightedFloatArrayAverage(
|
||||||
|
{10.0f, 20.0f}, {-1.0f, std::numeric_limits<float>::quiet_NaN()}, FloatResult));
|
||||||
|
TestTrue("A rejected weighted float average resets its output", FMath::IsNearlyZero(FloatResult));
|
||||||
|
|
||||||
|
VectorResult = FVector(123.0);
|
||||||
|
TestTrue("Weighted vector average ignores unusable weights",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetWeightedVectorArrayAverage(
|
||||||
|
{FVector(10.0, 20.0, 30.0), FVector(-4.0, 5.0, -6.0)},
|
||||||
|
{std::numeric_limits<float>::infinity(), 3.0f}, VectorResult)
|
||||||
|
&& VectorResult.Equals(FVector(-4.0, 5.0, -6.0), 1.e-9));
|
||||||
|
VectorResult = FVector(123.0);
|
||||||
|
TestFalse("Weighted vector average rejects arrays without a usable weight",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetWeightedVectorArrayAverage(
|
||||||
|
{FVector::ForwardVector}, {-1.0f}, VectorResult));
|
||||||
|
TestTrue("A rejected weighted vector average resets its output", VectorResult.IsZero());
|
||||||
|
|
||||||
|
FloatArrayResult = {123.0f};
|
||||||
|
TestFalse("Float array normalization rejects a non-finite source value",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::NormalizeFloatArrayToRange(
|
||||||
|
{1.0f, std::numeric_limits<float>::quiet_NaN()}, 0.0f, 1.0f, FloatArrayResult));
|
||||||
|
TestTrue("Rejected float array normalization clears its output", FloatArrayResult.IsEmpty());
|
||||||
|
TestFalse("Float array normalization rejects a non-finite output bound",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::NormalizeFloatArrayToRange(
|
||||||
|
{1.0f, 2.0f}, 0.0f, std::numeric_limits<float>::infinity(), FloatArrayResult));
|
||||||
|
|
||||||
|
TestTrue("Weight normalization ignores non-finite and negative weights",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::NormalizeWeights(
|
||||||
|
{std::numeric_limits<float>::quiet_NaN(), std::numeric_limits<float>::infinity(), -2.0f, 4.0f},
|
||||||
|
FloatArrayResult)
|
||||||
|
&& FloatArrayResult == TArray<float>({0.0f, 0.0f, 0.0f, 1.0f}));
|
||||||
|
FloatArrayResult = {123.0f};
|
||||||
|
TestFalse("Weight normalization rejects an empty array",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::NormalizeWeights({}, FloatArrayResult));
|
||||||
|
TestTrue("Rejected weight normalization clears its output", FloatArrayResult.IsEmpty());
|
||||||
|
|
||||||
|
TestTrue("Percentiles clamp below zero",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile({7.0f, 3.0f, 11.0f}, -50.0f, FloatResult)
|
||||||
|
&& FMath::IsNearlyEqual(FloatResult, 3.0f));
|
||||||
|
TestTrue("Percentiles return the maximum at one hundred",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile({7.0f, 3.0f, 11.0f}, 100.0f, FloatResult)
|
||||||
|
&& FMath::IsNearlyEqual(FloatResult, 11.0f));
|
||||||
|
TestTrue("A single-value percentile is stable at every finite percentile",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile({-7.5f}, 37.25f, FloatResult)
|
||||||
|
&& FMath::IsNearlyEqual(FloatResult, -7.5f));
|
||||||
|
FloatResult = 123.0f;
|
||||||
|
TestFalse("Percentiles reject a non-finite percentile",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile(
|
||||||
|
{1.0f, 2.0f}, std::numeric_limits<float>::quiet_NaN(), FloatResult));
|
||||||
|
TestTrue("A rejected percentile resets its output", FMath::IsNearlyZero(FloatResult));
|
||||||
|
|
||||||
|
const float LargeFiniteValue = std::numeric_limits<float>::max() * 0.25f;
|
||||||
|
TestTrue("Root mean square remains finite near the float limit",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetFloatArrayRootMeanSquare(
|
||||||
|
{LargeFiniteValue, -LargeFiniteValue}, FloatResult)
|
||||||
|
&& FMath::IsFinite(FloatResult)
|
||||||
|
&& FMath::IsNearlyEqual(FloatResult / LargeFiniteValue, 1.0f, 1.e-5f));
|
||||||
|
FloatResult = 123.0f;
|
||||||
|
TestFalse("Root mean square rejects non-finite values",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetFloatArrayRootMeanSquare(
|
||||||
|
{1.0f, std::numeric_limits<float>::infinity()}, FloatResult));
|
||||||
|
TestTrue("A rejected root mean square resets its output", FMath::IsNearlyZero(FloatResult));
|
||||||
|
|
||||||
|
TestTrue("Signed angle is invariant under positive vector scaling",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors(
|
||||||
|
FVector(20.0, 0.0, 5.0), FVector(0.0, 30.0, -7.0), FVector(0.0, 0.0, 9.0)), 90.0f, 1.e-4f));
|
||||||
|
TestTrue("A half-turn signed angle has the expected magnitude",
|
||||||
|
FMath::IsNearlyEqual(FMath::Abs(UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors(
|
||||||
|
FVector::ForwardVector, -FVector::ForwardVector, FVector::UpVector)), 180.0f, 1.e-4f));
|
||||||
|
TestEqual("Signed angle rejects non-finite vectors",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors(
|
||||||
|
FVector(std::numeric_limits<double>::infinity(), 0.0, 0.0), FVector::RightVector, FVector::UpVector),
|
||||||
|
0.0f);
|
||||||
|
TestTrue("Delta angle ignores complete turns",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::DeltaAngle(-1080.0f + 15.0f, 1440.0f - 25.0f), -40.0f));
|
||||||
|
TestTrue("Angle interpolation permits negative extrapolation",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::LerpAngle(10.0f, 350.0f, -1.0f), 30.0f));
|
||||||
|
TestTrue("Angle interpolation ignores complete turns in the delta",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::LerpAngle(
|
||||||
|
-1080.0f + 15.0f, 1440.0f - 25.0f, 0.5f), -1085.0f));
|
||||||
|
|
||||||
|
TestTrue("Ping Pong repeats across multiple positive periods",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::PingPong(123.0f, -2.0f, 3.0f), 3.0f));
|
||||||
|
TestTrue("Ping Pong repeats across multiple negative periods",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::PingPong(-122.0f, -2.0f, 3.0f), -2.0f));
|
||||||
|
TestTrue("Smooth Step clamps above its range",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SmoothStep(100.0f, -2.0f, 3.0f), 1.0f));
|
||||||
|
TestTrue("Smoother Step accepts reversed bounds",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SmootherStep(0.25f, 1.0f, 0.0f), 0.103515625f));
|
||||||
|
TestTrue("Step functions reject non-finite values",
|
||||||
|
FMath::IsNearlyZero(UDirectiveUtilMathFunctionLibrary::SmoothStep(
|
||||||
|
std::numeric_limits<float>::quiet_NaN(), 0.0f, 1.0f))
|
||||||
|
&& FMath::IsNearlyZero(UDirectiveUtilMathFunctionLibrary::SmootherStep(
|
||||||
|
0.5f, 0.0f, std::numeric_limits<float>::infinity())));
|
||||||
|
|
||||||
|
TestTrue("Range Falloff accepts reversed radii",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RangeFalloff(5.0f, 10.0f, 0.0f), 0.5f));
|
||||||
|
TestTrue("Range Falloff clamps negative distances to zero",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RangeFalloff(-5.0f, 2.0f, 10.0f), 1.0f));
|
||||||
|
TestTrue("Range Falloff treats non-positive exponents as a hard inner range",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RangeFalloff(9.0f, 2.0f, 10.0f, -3.0f), 1.0f));
|
||||||
|
TestTrue("Range Falloff is full strength at a collapsed shared radius",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RangeFalloff(5.0f, 5.0f, 5.0f), 1.0f));
|
||||||
|
TestTrue("Range Falloff is full strength at the origin when both radii are zero",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RangeFalloff(0.0f, 0.0f, 0.0f), 1.0f));
|
||||||
|
TestTrue("Range Falloff is zero outside a collapsed shared radius",
|
||||||
|
FMath::IsNearlyZero(UDirectiveUtilMathFunctionLibrary::RangeFalloff(6.0f, 5.0f, 5.0f)));
|
||||||
|
|
||||||
|
TestTrue("A full-width cone includes the opposite direction",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone(
|
||||||
|
-FVector::ForwardVector, FVector::ForwardVector, 180.0f));
|
||||||
|
TestFalse("A zero-width cone excludes the opposite direction",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone(
|
||||||
|
-FVector::ForwardVector, FVector::ForwardVector, 0.0f));
|
||||||
|
TestTrue("A point on both cone boundaries is included",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::IsPointWithinCone(
|
||||||
|
FVector(5.0, 5.0, 0.0), FVector::ZeroVector, FVector::ForwardVector, 45.0f,
|
||||||
|
FMath::Sqrt(50.0)));
|
||||||
|
TestTrue("Negative cone distance is treated as unlimited",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::IsPointWithinCone(
|
||||||
|
FVector(100.0, 0.0, 0.0), FVector::ZeroVector, FVector::ForwardVector, 0.0f, -1.0));
|
||||||
|
|
||||||
|
TestTrue("Rotating by a complete turn preserves a translated point",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::RotatePointAroundPivot2D(
|
||||||
|
FVector2D(1000003.0, -1999995.0), FVector2D(1000000.0, -2000000.0), 1080.0f)
|
||||||
|
.Equals(FVector2D(1000003.0, -1999995.0), 1.e-8));
|
||||||
|
TestTrue("Signed plane distance is translation invariant",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedDistanceToPlane(
|
||||||
|
FVector(1000000.0, -2000000.0, 3000007.0), FVector(1000000.0, -2000000.0, 3000000.0),
|
||||||
|
FVector(0.0, 0.0, 123.0)), 7.0, 1.e-9));
|
||||||
|
|
||||||
|
FRandomStream PositiveRadiusStream(424242);
|
||||||
|
FRandomStream NegativeRadiusStream(424242);
|
||||||
|
TestTrue("Random circle stream treats negative radius as magnitude",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream(PositiveRadiusStream, 5.0f).Equals(
|
||||||
|
UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream(NegativeRadiusStream, -5.0f), 1.e-12));
|
||||||
|
FRandomStream OrderedAnnulusStream(31337);
|
||||||
|
FRandomStream ReversedAnnulusStream(31337);
|
||||||
|
TestTrue("Random annulus stream accepts negative reversed radii",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream(OrderedAnnulusStream, 2.0f, 5.0f).Equals(
|
||||||
|
UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream(ReversedAnnulusStream, -5.0f, -2.0f),
|
||||||
|
1.e-12));
|
||||||
|
FRandomStream PositiveSphereStream(8675309);
|
||||||
|
FRandomStream NegativeSphereStream(8675309);
|
||||||
|
TestTrue("Random sphere stream treats negative radius as magnitude",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream(PositiveSphereStream, 5.0f).Equals(
|
||||||
|
UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream(NegativeSphereStream, -5.0f), 1.e-12));
|
||||||
|
|
||||||
|
return !HasAnyErrors();
|
||||||
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
#include "Libraries/DirectiveUtilMathFunctionLibrary.h"
|
#include "Libraries/DirectiveUtilMathFunctionLibrary.h"
|
||||||
#include "Misc/AutomationTest.h"
|
#include "Misc/AutomationTest.h"
|
||||||
|
|
||||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMathFunctionLibraryTest, "DirectiveUtilities.MathFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
#include <limits>
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMathFunctionLibraryTest, "DirectiveUtilities.MathFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
bool FDirectiveUtilMathFunctionLibraryTest::RunTest(const FString& Parameters)
|
bool FDirectiveUtilMathFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||||
{
|
{
|
||||||
@@ -11,8 +15,84 @@ bool FDirectiveUtilMathFunctionLibraryTest::RunTest(const FString& Parameters)
|
|||||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ForwardVector, FVector::RightVector), 90.0f, 0.01f));
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ForwardVector, FVector::RightVector), 90.0f, 0.01f));
|
||||||
TestTrue("AngleBetweenVectors should return ~180 for opposite vectors",
|
TestTrue("AngleBetweenVectors should return ~180 for opposite vectors",
|
||||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ForwardVector, -FVector::ForwardVector), 180.0f, 0.01f));
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ForwardVector, -FVector::ForwardVector), 180.0f, 0.01f));
|
||||||
TestTrue("AngleBetweenVectors should handle zero vector gracefully",
|
TestEqual("AngleBetweenVectors should return 0 for a zero vector",
|
||||||
FMath::IsFinite(UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ZeroVector, FVector::ForwardVector)));
|
UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ZeroVector, FVector::ForwardVector), 0.0f);
|
||||||
|
TestEqual("AngleBetweenVectors should return 0 for two zero vectors",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ZeroVector, FVector::ZeroVector), 0.0f);
|
||||||
|
|
||||||
|
TestTrue("SignedAngleBetweenVectors returns a positive counterclockwise angle around the axis",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors(
|
||||||
|
FVector::ForwardVector, FVector::RightVector, FVector::UpVector), 90.0f, 1.e-4f));
|
||||||
|
TestTrue("SignedAngleBetweenVectors returns a negative clockwise angle around the axis",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors(
|
||||||
|
FVector::RightVector, FVector::ForwardVector, FVector::UpVector), -90.0f, 1.e-4f));
|
||||||
|
TestTrue("SignedAngleBetweenVectors reverses sign with the axis",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors(
|
||||||
|
FVector::ForwardVector, FVector::RightVector, -FVector::UpVector), -90.0f, 1.e-4f));
|
||||||
|
TestTrue("SignedAngleBetweenVectors projects directions onto the axis plane",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors(
|
||||||
|
FVector(1.0, 0.0, 4.0), FVector(0.0, 1.0, -3.0), FVector::UpVector), 90.0f, 1.e-4f));
|
||||||
|
TestEqual("SignedAngleBetweenVectors returns zero for a zero direction",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors(FVector::ZeroVector, FVector::RightVector, FVector::UpVector), 0.0f);
|
||||||
|
TestEqual("SignedAngleBetweenVectors returns zero for a direction parallel to the axis",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors(FVector::UpVector, FVector::RightVector, FVector::UpVector), 0.0f);
|
||||||
|
TestEqual("SignedAngleBetweenVectors returns zero for a zero axis",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors(FVector::ForwardVector, FVector::RightVector, FVector::ZeroVector), 0.0f);
|
||||||
|
|
||||||
|
TestTrue("DeltaAngle crosses the positive angle seam by the shortest path",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::DeltaAngle(350.0f, 10.0f), 20.0f, 1.e-4f));
|
||||||
|
TestTrue("DeltaAngle crosses the negative angle seam by the shortest path",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::DeltaAngle(10.0f, 350.0f), -20.0f, 1.e-4f));
|
||||||
|
TestEqual("DeltaAngle returns zero for equivalent wrapped angles",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::DeltaAngle(-180.0f, 180.0f), 0.0f);
|
||||||
|
TestTrue("DeltaAngle canonicalizes an exactly opposite pair to +180",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::DeltaAngle(0.0f, 180.0f), 180.0f, 1.e-4f)
|
||||||
|
&& FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::DeltaAngle(0.0f, -180.0f), 180.0f, 1.e-4f)
|
||||||
|
&& FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::DeltaAngle(0.0f, 540.0f), 180.0f, 1.e-4f)
|
||||||
|
&& FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::DeltaAngle(0.0f, -540.0f), 180.0f, 1.e-4f));
|
||||||
|
TestEqual("DeltaAngle returns zero for non-finite input",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::DeltaAngle(std::numeric_limits<float>::infinity(), 0.0f), 0.0f);
|
||||||
|
|
||||||
|
TestTrue("LerpAngle crosses the angle seam by the shortest path",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::LerpAngle(350.0f, 10.0f, 0.5f), 360.0f, 1.e-4f));
|
||||||
|
TestTrue("LerpAngle returns A at Alpha 0",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::LerpAngle(350.0f, 10.0f, 0.0f), 350.0f, 1.e-4f));
|
||||||
|
TestTrue("LerpAngle reaches A plus the shortest delta at Alpha 1",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::LerpAngle(350.0f, 10.0f, 1.0f), 370.0f, 1.e-4f));
|
||||||
|
TestTrue("LerpAngle permits extrapolation without wrapping the result",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::LerpAngle(0.0f, 90.0f, 2.0f), 180.0f, 1.e-4f)
|
||||||
|
&& FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::LerpAngle(0.0f, 90.0f, 3.0f), 270.0f, 1.e-4f));
|
||||||
|
TestEqual("LerpAngle returns zero for non-finite input",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::LerpAngle(0.0f, 90.0f, std::numeric_limits<float>::quiet_NaN()), 0.0f);
|
||||||
|
|
||||||
|
TestTrue("PingPong reaches the middle of an ascending range",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::PingPong(0.5f, 0.0f, 1.0f), 0.5f, 1.e-4f));
|
||||||
|
TestTrue("PingPong reverses after the upper bound",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::PingPong(1.5f, 0.0f, 1.0f), 0.5f, 1.e-4f));
|
||||||
|
TestTrue("PingPong supports negative values",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::PingPong(-0.25f, 0.0f, 1.0f), 0.25f, 1.e-4f));
|
||||||
|
TestTrue("PingPong accepts reversed bounds",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::PingPong(12.5f, 20.0f, 10.0f), 12.5f, 1.e-4f));
|
||||||
|
TestEqual("PingPong returns the shared bound for a zero-sized range",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::PingPong(100.0f, 7.0f, 7.0f), 7.0f);
|
||||||
|
TestEqual("PingPong returns zero for non-finite input",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::PingPong(std::numeric_limits<float>::infinity(), 0.0f, 1.0f), 0.0f);
|
||||||
|
|
||||||
|
TestTrue("IsDirectionWithinCone includes a direction inside the cone",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone(FVector(1.0, 1.0, 0.0), FVector::ForwardVector, 46.0f));
|
||||||
|
TestTrue("IsDirectionWithinCone includes a direction on the cone boundary",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone(FVector(1.0, 1.0, 0.0), FVector::ForwardVector, 45.0f));
|
||||||
|
TestFalse("IsDirectionWithinCone excludes a direction outside the cone",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone(FVector::RightVector, FVector::ForwardVector, 45.0f));
|
||||||
|
TestTrue("IsDirectionWithinCone clamps angles above 180 degrees",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone(-FVector::ForwardVector, FVector::ForwardVector, 270.0f));
|
||||||
|
TestFalse("IsDirectionWithinCone clamps negative angles to zero",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone(FVector(1.0, 0.1, 0.0), FVector::ForwardVector, -20.0f));
|
||||||
|
TestFalse("IsDirectionWithinCone rejects a zero direction",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone(FVector::ZeroVector, FVector::ForwardVector, 45.0f));
|
||||||
|
TestFalse("IsDirectionWithinCone rejects a non-finite angle",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone(
|
||||||
|
FVector::ForwardVector, FVector::ForwardVector, std::numeric_limits<float>::quiet_NaN()));
|
||||||
|
|
||||||
{
|
{
|
||||||
const FVector2D Sample2D(12.34f, 56.78f);
|
const FVector2D Sample2D(12.34f, 56.78f);
|
||||||
@@ -75,15 +155,16 @@ bool FDirectiveUtilMathFunctionLibraryTest::RunTest(const FString& Parameters)
|
|||||||
const TArray<EDirectiveUtilEaseType> AllEaseTypes = {
|
const TArray<EDirectiveUtilEaseType> AllEaseTypes = {
|
||||||
EDirectiveUtilEaseType::BackIn, EDirectiveUtilEaseType::BackOut, EDirectiveUtilEaseType::BackInOut,
|
EDirectiveUtilEaseType::BackIn, EDirectiveUtilEaseType::BackOut, EDirectiveUtilEaseType::BackInOut,
|
||||||
EDirectiveUtilEaseType::ElasticIn, EDirectiveUtilEaseType::ElasticOut, EDirectiveUtilEaseType::ElasticInOut,
|
EDirectiveUtilEaseType::ElasticIn, EDirectiveUtilEaseType::ElasticOut, EDirectiveUtilEaseType::ElasticInOut,
|
||||||
EDirectiveUtilEaseType::BounceIn, EDirectiveUtilEaseType::BounceOut, EDirectiveUtilEaseType::BounceInOut
|
EDirectiveUtilEaseType::BounceIn, EDirectiveUtilEaseType::BounceOut, EDirectiveUtilEaseType::BounceInOut,
|
||||||
|
EDirectiveUtilEaseType::Linear
|
||||||
};
|
};
|
||||||
for (const EDirectiveUtilEaseType EaseType : AllEaseTypes)
|
for (const EDirectiveUtilEaseType EaseType : AllEaseTypes)
|
||||||
{
|
{
|
||||||
const FString TypeName = FString::FromInt(static_cast<int32>(EaseType));
|
const FString TypeName = FString::FromInt(static_cast<int32>(EaseType));
|
||||||
TestTrue(FString::Printf(TEXT("EaseAlpha(0) should be ~0 for type %s"), *TypeName),
|
TestEqual(FString::Printf(TEXT("EaseAlpha(0) should be exactly 0 for type %s"), *TypeName),
|
||||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::EaseAlpha(0.0f, EaseType), 0.0f, 1.e-3f));
|
UDirectiveUtilMathFunctionLibrary::EaseAlpha(0.0f, EaseType), 0.0f);
|
||||||
TestTrue(FString::Printf(TEXT("EaseAlpha(1) should be ~1 for type %s"), *TypeName),
|
TestEqual(FString::Printf(TEXT("EaseAlpha(1) should be exactly 1 for type %s"), *TypeName),
|
||||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::EaseAlpha(1.0f, EaseType), 1.0f, 1.e-3f));
|
UDirectiveUtilMathFunctionLibrary::EaseAlpha(1.0f, EaseType), 1.0f);
|
||||||
for (const float Sample : {0.0f, 0.25f, 0.5f, 0.75f, 1.0f})
|
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),
|
TestTrue(FString::Printf(TEXT("EaseAlpha(%.2f) should be finite for type %s"), Sample, *TypeName),
|
||||||
@@ -94,6 +175,22 @@ bool FDirectiveUtilMathFunctionLibraryTest::RunTest(const FString& Parameters)
|
|||||||
TestEqual("EaseAlpha should clamp alpha below 0",
|
TestEqual("EaseAlpha should clamp alpha below 0",
|
||||||
UDirectiveUtilMathFunctionLibrary::EaseAlpha(-1.0f, EDirectiveUtilEaseType::BounceOut),
|
UDirectiveUtilMathFunctionLibrary::EaseAlpha(-1.0f, EDirectiveUtilEaseType::BounceOut),
|
||||||
UDirectiveUtilMathFunctionLibrary::EaseAlpha(0.0f, EDirectiveUtilEaseType::BounceOut));
|
UDirectiveUtilMathFunctionLibrary::EaseAlpha(0.0f, EDirectiveUtilEaseType::BounceOut));
|
||||||
|
TestEqual("Linear ease should pass the clamped alpha through",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::EaseAlpha(0.3f, EDirectiveUtilEaseType::Linear), 0.3f);
|
||||||
|
|
||||||
|
const FTransform EaseStart(FRotator::ZeroRotator, FVector::ZeroVector, FVector::OneVector);
|
||||||
|
const FTransform EaseTarget(FRotator(0.0, 90.0, 0.0), FVector(10.0, 0.0, 0.0), FVector(3.0));
|
||||||
|
const FTransform EasedMidpoint = UDirectiveUtilMathFunctionLibrary::EaseTransform(
|
||||||
|
EaseStart, EaseTarget, 0.5f, EDirectiveUtilEaseType::Linear);
|
||||||
|
TestTrue("EaseTransform should blend location, rotation, and scale",
|
||||||
|
EasedMidpoint.GetLocation().Equals(FVector(5.0, 0.0, 0.0), 1.e-4)
|
||||||
|
&& EasedMidpoint.GetRotation().Equals(FRotator(0.0, 45.0, 0.0).Quaternion(), 1.e-4)
|
||||||
|
&& EasedMidpoint.GetScale3D().Equals(FVector(2.0), 1.e-4));
|
||||||
|
TestTrue("EaseTransform should return its endpoints at alpha 0 and 1",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::EaseTransform(
|
||||||
|
EaseStart, EaseTarget, 0.0f, EDirectiveUtilEaseType::BounceOut).Equals(EaseStart, 1.e-4)
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::EaseTransform(
|
||||||
|
EaseStart, EaseTarget, 1.0f, EDirectiveUtilEaseType::BounceOut).Equals(EaseTarget, 1.e-4));
|
||||||
TestEqual("EaseAlpha should clamp alpha above 1",
|
TestEqual("EaseAlpha should clamp alpha above 1",
|
||||||
UDirectiveUtilMathFunctionLibrary::EaseAlpha(2.0f, EDirectiveUtilEaseType::BounceOut),
|
UDirectiveUtilMathFunctionLibrary::EaseAlpha(2.0f, EDirectiveUtilEaseType::BounceOut),
|
||||||
UDirectiveUtilMathFunctionLibrary::EaseAlpha(1.0f, EDirectiveUtilEaseType::BounceOut));
|
UDirectiveUtilMathFunctionLibrary::EaseAlpha(1.0f, EDirectiveUtilEaseType::BounceOut));
|
||||||
@@ -147,6 +244,10 @@ bool FDirectiveUtilMathFunctionLibraryTest::RunTest(const FString& Parameters)
|
|||||||
UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights(TArray<float>()), static_cast<int32>(INDEX_NONE));
|
UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights(TArray<float>()), static_cast<int32>(INDEX_NONE));
|
||||||
TestEqual("Weighted random with all-zero weights returns 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));
|
UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights({0.0f, 0.0f, 0.0f}), static_cast<int32>(INDEX_NONE));
|
||||||
|
TestEqual("Weighted random ignores non-finite weights",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights({std::numeric_limits<float>::quiet_NaN(), 1.0f, std::numeric_limits<float>::infinity()}), 1);
|
||||||
|
TestTrue("Weighted random handles large finite totals",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights({MAX_flt, MAX_flt}) != INDEX_NONE);
|
||||||
for (int32 Iteration = 0; Iteration < 25; ++Iteration)
|
for (int32 Iteration = 0; Iteration < 25; ++Iteration)
|
||||||
{
|
{
|
||||||
TestEqual("Weighted random {0,1,0} always selects index 1",
|
TestEqual("Weighted random {0,1,0} always selects index 1",
|
||||||
@@ -172,6 +273,10 @@ bool FDirectiveUtilMathFunctionLibraryTest::RunTest(const FString& Parameters)
|
|||||||
UDirectiveUtilMathFunctionLibrary::FormatDuration(45.0f).ToString(), FString(TEXT("45s")));
|
UDirectiveUtilMathFunctionLibrary::FormatDuration(45.0f).ToString(), FString(TEXT("45s")));
|
||||||
TestEqual("FormatDuration(-90) is \"-1m 30s\"",
|
TestEqual("FormatDuration(-90) is \"-1m 30s\"",
|
||||||
UDirectiveUtilMathFunctionLibrary::FormatDuration(-90.0f).ToString(), FString(TEXT("-1m 30s")));
|
UDirectiveUtilMathFunctionLibrary::FormatDuration(-90.0f).ToString(), FString(TEXT("-1m 30s")));
|
||||||
|
TestEqual("FormatDuration(-45, false) is \"0m\"",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::FormatDuration(-45.0f, false).ToString(), FString(TEXT("0m")));
|
||||||
|
TestFalse("FormatDuration handles the largest finite float without wrapping negative",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::FormatDuration(MAX_flt).ToString().StartsWith(TEXT("-")));
|
||||||
|
|
||||||
TestEqual("FormatRelativeTime 5 minutes back reads \"5 minutes ago\"",
|
TestEqual("FormatRelativeTime 5 minutes back reads \"5 minutes ago\"",
|
||||||
UDirectiveUtilMathFunctionLibrary::FormatRelativeTime(FDateTime::Now() - FTimespan::FromMinutes(5)).ToString(), FString(TEXT("5 minutes ago")));
|
UDirectiveUtilMathFunctionLibrary::FormatRelativeTime(FDateTime::Now() - FTimespan::FromMinutes(5)).ToString(), FString(TEXT("5 minutes ago")));
|
||||||
@@ -231,6 +336,50 @@ bool FDirectiveUtilMathFunctionLibraryTest::RunTest(const FString& Parameters)
|
|||||||
UDirectiveUtilMathFunctionLibrary::GetFloatArrayStandardDeviation(EmptyFloats), 0.0f);
|
UDirectiveUtilMathFunctionLibrary::GetFloatArrayStandardDeviation(EmptyFloats), 0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TestTrue("GetIntArrayMedian should average the full int32 range without overflow",
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian({MIN_int32, MAX_int32}), -0.5f, 1.e-4f));
|
||||||
|
TestEqual("GetIntArrayMedian should handle repeated values",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian({7, 7, 7, 7, 7}), 7.0f);
|
||||||
|
TestEqual("GetFloatArrayMedian should handle repeated values",
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetFloatArrayMedian({7.5f, 7.5f, 7.5f, 7.5f}), 7.5f);
|
||||||
|
TestTrue("GetFloatArrayMedian should return NaN when any input is NaN",
|
||||||
|
FMath::IsNaN(UDirectiveUtilMathFunctionLibrary::GetFloatArrayMedian({1.0f, std::numeric_limits<float>::quiet_NaN(), 3.0f})));
|
||||||
|
|
||||||
|
FRandomStream MedianStream(481516);
|
||||||
|
for (int32 Iteration = 0; Iteration < 200; ++Iteration)
|
||||||
|
{
|
||||||
|
const int32 Count = MedianStream.RandRange(1, 257);
|
||||||
|
TArray<int32> IntValues;
|
||||||
|
TArray<float> FloatValues;
|
||||||
|
IntValues.Reserve(Count);
|
||||||
|
FloatValues.Reserve(Count);
|
||||||
|
for (int32 Index = 0; Index < Count; ++Index)
|
||||||
|
{
|
||||||
|
const int32 Value = MedianStream.RandRange(-1000, 1000);
|
||||||
|
IntValues.Add(Value);
|
||||||
|
FloatValues.Add(static_cast<float>(Value) * 0.25f);
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<int32> SortedInts = IntValues;
|
||||||
|
SortedInts.Sort();
|
||||||
|
const int32 Middle = SortedInts.Num() / 2;
|
||||||
|
const float ExpectedIntMedian = SortedInts.Num() % 2 == 0
|
||||||
|
? static_cast<float>((static_cast<double>(SortedInts[Middle - 1]) + static_cast<double>(SortedInts[Middle])) * 0.5)
|
||||||
|
: static_cast<float>(SortedInts[Middle]);
|
||||||
|
TestTrue(
|
||||||
|
FString::Printf(TEXT("Integer median fuzz case %d"), Iteration),
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian(IntValues), ExpectedIntMedian, 1.e-4f));
|
||||||
|
|
||||||
|
TArray<float> SortedFloats = FloatValues;
|
||||||
|
SortedFloats.Sort();
|
||||||
|
const float ExpectedFloatMedian = SortedFloats.Num() % 2 == 0
|
||||||
|
? static_cast<float>((static_cast<double>(SortedFloats[Middle - 1]) + static_cast<double>(SortedFloats[Middle])) * 0.5)
|
||||||
|
: SortedFloats[Middle];
|
||||||
|
TestTrue(
|
||||||
|
FString::Printf(TEXT("Float median fuzz case %d"), Iteration),
|
||||||
|
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::GetFloatArrayMedian(FloatValues), ExpectedFloatMedian, 1.e-4f));
|
||||||
|
}
|
||||||
|
|
||||||
FRandomStream StreamA(12345);
|
FRandomStream StreamA(12345);
|
||||||
FRandomStream StreamB(12345);
|
FRandomStream StreamB(12345);
|
||||||
const int32 StreamIndexA = UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeightsFromStream(StreamA, {1.0f, 1.0f, 1.0f, 1.0f});
|
const int32 StreamIndexA = UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeightsFromStream(StreamA, {1.0f, 1.0f, 1.0f, 1.0f});
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "Libraries/DirectiveUtilMathFunctionLibrary.h"
|
||||||
|
|
||||||
|
#include "Misc/AutomationTest.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
template <typename ValueType>
|
||||||
|
double GetSortedMedian(TArray<ValueType> Values)
|
||||||
|
{
|
||||||
|
Values.Sort();
|
||||||
|
const int32 Middle = Values.Num() / 2;
|
||||||
|
if (Values.Num() % 2 != 0)
|
||||||
|
{
|
||||||
|
return static_cast<double>(Values[Middle]);
|
||||||
|
}
|
||||||
|
return (static_cast<double>(Values[Middle - 1]) + static_cast<double>(Values[Middle])) * 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<int32> MakeMedianValues(const int32 Count, const int32 Pattern)
|
||||||
|
{
|
||||||
|
TArray<int32> Values;
|
||||||
|
Values.Reserve(Count);
|
||||||
|
for (int32 Index = 0; Index < Count; ++Index)
|
||||||
|
{
|
||||||
|
switch (Pattern)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
Values.Add(Index - Count / 2);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
Values.Add(Count - Index);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
Values.Add(Index % 7);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Values.Add(Index % 2 == 0 ? MIN_int32 : MAX_int32);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Values;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
|
FDirectiveUtilMathCardinalityTest,
|
||||||
|
"DirectiveUtilities.MathScenarios.MedianCardinality",
|
||||||
|
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FDirectiveUtilMathCardinalityTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
for (const int32 ItemCount : {1, 2, 3, 4, 5, 16, 17, 256, 257, 4096, 4097})
|
||||||
|
{
|
||||||
|
for (int32 Pattern = 0; Pattern < 4; ++Pattern)
|
||||||
|
{
|
||||||
|
const TArray<int32> IntegerValues = MakeMedianValues(ItemCount, Pattern);
|
||||||
|
TArray<float> FloatValues;
|
||||||
|
FloatValues.Reserve(ItemCount);
|
||||||
|
for (const int32 Value : IntegerValues)
|
||||||
|
{
|
||||||
|
FloatValues.Add(static_cast<float>(Value) * 0.25f);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FString Label = FString::Printf(TEXT("items=%d pattern=%d"), ItemCount, Pattern);
|
||||||
|
TestTrue(
|
||||||
|
Label + TEXT(" integer median"),
|
||||||
|
FMath::IsNearlyEqual(
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian(IntegerValues),
|
||||||
|
static_cast<float>(GetSortedMedian(IntegerValues)),
|
||||||
|
1.e-4f));
|
||||||
|
TestTrue(
|
||||||
|
Label + TEXT(" float median"),
|
||||||
|
FMath::IsNearlyEqual(
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetFloatArrayMedian(FloatValues),
|
||||||
|
static_cast<float>(GetSortedMedian(FloatValues)),
|
||||||
|
1.e-4f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,837 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "Libraries/DirectiveUtilMathFunctionLibrary.h"
|
||||||
|
|
||||||
|
#include "Components/SplineComponent.h"
|
||||||
|
#include "Misc/AutomationTest.h"
|
||||||
|
#include "UObject/Class.h"
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
bool PointsEqual(const TArray<FVector>& A, const TArray<FVector>& B, const double Tolerance = 1.e-9)
|
||||||
|
{
|
||||||
|
if (A.Num() != B.Num())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int32 Index = 0; Index < A.Num(); ++Index)
|
||||||
|
{
|
||||||
|
if (!A[Index].Equals(B[Index], Tolerance))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PointsEqualAfterTranslation(const TArray<FVector>& BasePoints, const TArray<FVector>& TranslatedPoints,
|
||||||
|
const FVector& Translation, const double Tolerance = 1.e-6)
|
||||||
|
{
|
||||||
|
if (BasePoints.Num() != TranslatedPoints.Num())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int32 Index = 0; Index < BasePoints.Num(); ++Index)
|
||||||
|
{
|
||||||
|
if (!(TranslatedPoints[Index] - Translation).Equals(BasePoints[Index], Tolerance))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PointsMatchLocation(const TArray<FVector>& Points, const FVector& Location, const double Tolerance = 1.e-9)
|
||||||
|
{
|
||||||
|
for (const FVector& Point : Points)
|
||||||
|
{
|
||||||
|
if (!Point.Equals(Location, Tolerance))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
|
FDirectiveUtilPointGenerationTest,
|
||||||
|
"DirectiveUtilities.Math.PointGeneration",
|
||||||
|
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FDirectiveUtilPointGenerationTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
const FName CallableGeneratorNames[] = {
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateGridPoints2D),
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateGridPoints3D),
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateGridTransforms2D),
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateGridTransforms3D),
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateRectangularHexGrid),
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateRectangularHexGridTransforms),
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GetRectangularHexGridCoordinates),
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateHexagonalHexGrid),
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateHexagonalHexGridTransforms),
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GetHexesInRange),
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GetHexRing),
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GetHexLine),
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GeneratePointsAlongDirection),
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GeneratePointsBetweenLocations),
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GeneratePointsOnCircle),
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateTransformsOnCircle),
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GeneratePointsOnArc),
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateTransformsOnArc),
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GeneratePointsOnDisc),
|
||||||
|
GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GeneratePointsOnSphere)
|
||||||
|
};
|
||||||
|
for (const FName FunctionName : CallableGeneratorNames)
|
||||||
|
{
|
||||||
|
const UFunction* Function = UDirectiveUtilMathFunctionLibrary::StaticClass()->FindFunctionByName(FunctionName);
|
||||||
|
TestNotNull(*FString::Printf(TEXT("%s should be exposed to Blueprint"), *FunctionName.ToString()), Function);
|
||||||
|
if (Function)
|
||||||
|
{
|
||||||
|
TestTrue(*FString::Printf(TEXT("%s should be callable"), *FunctionName.ToString()),
|
||||||
|
Function->HasAnyFunctionFlags(FUNC_BlueprintCallable));
|
||||||
|
TestFalse(*FString::Printf(TEXT("%s should not execute as a pure node"), *FunctionName.ToString()),
|
||||||
|
Function->HasAnyFunctionFlags(FUNC_BlueprintPure));
|
||||||
|
#if WITH_EDITOR
|
||||||
|
TestFalse(*FString::Printf(TEXT("%s should not advertise inert Blueprint thread safety"), *FunctionName.ToString()),
|
||||||
|
Function->HasMetaData(TEXT("BlueprintThreadSafe")));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TArray<FVector> Grid2D = UDirectiveUtilMathFunctionLibrary::GenerateGridPoints2D(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(3, 2), FVector2D(10.0, 20.0), true);
|
||||||
|
const TArray<FVector> ExpectedGrid2D = {
|
||||||
|
FVector(-10.0, -10.0, 0.0), FVector(0.0, -10.0, 0.0), FVector(10.0, -10.0, 0.0),
|
||||||
|
FVector(-10.0, 10.0, 0.0), FVector(0.0, 10.0, 0.0), FVector(10.0, 10.0, 0.0)
|
||||||
|
};
|
||||||
|
TestTrue(TEXT("2D grid is centered and ordered by X then Y"), PointsEqual(Grid2D, ExpectedGrid2D));
|
||||||
|
|
||||||
|
const TArray<FVector> RotatedGrid2D = UDirectiveUtilMathFunctionLibrary::GenerateGridPoints2D(
|
||||||
|
FVector(5.0, 7.0, 9.0), FRotator(0.0, 90.0, 0.0), FIntPoint(2, 1), FVector2D(3.0, 0.0), false);
|
||||||
|
TestTrue(TEXT("2D grid rotation places its local X axis in world space"),
|
||||||
|
RotatedGrid2D.Num() == 2
|
||||||
|
&& RotatedGrid2D[0].Equals(FVector(5.0, 7.0, 9.0), 1.e-9)
|
||||||
|
&& RotatedGrid2D[1].Equals(FVector(5.0, 10.0, 9.0), 1.e-9));
|
||||||
|
|
||||||
|
const TArray<FVector> Grid3D = UDirectiveUtilMathFunctionLibrary::GenerateGridPoints3D(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, FIntVector(2, 2, 2), FVector(2.0, 4.0, 6.0), true);
|
||||||
|
TestTrue(TEXT("3D grid is centered and ordered by X then Y then Z"),
|
||||||
|
Grid3D.Num() == 8
|
||||||
|
&& Grid3D[0].Equals(FVector(-1.0, -2.0, -3.0), 1.e-9)
|
||||||
|
&& Grid3D[1].Equals(FVector(1.0, -2.0, -3.0), 1.e-9)
|
||||||
|
&& Grid3D[2].Equals(FVector(-1.0, 2.0, -3.0), 1.e-9)
|
||||||
|
&& Grid3D.Last().Equals(FVector(1.0, 2.0, 3.0), 1.e-9));
|
||||||
|
TestTrue(TEXT("Grid generation rejects non-positive dimensions"),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GenerateGridPoints2D(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(2, 0), FVector2D(1.0), true).IsEmpty());
|
||||||
|
TestTrue(TEXT("Grid generation rejects point-count overflow"),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GenerateGridPoints3D(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, FIntVector(MAX_int32, 2, 2), FVector::OneVector, true).IsEmpty());
|
||||||
|
|
||||||
|
const FRotator GridInstanceRotation(10.0, 20.0, 30.0);
|
||||||
|
const FQuat GridInstanceQuaternion = GridInstanceRotation.Quaternion();
|
||||||
|
const FVector GridInstanceScale(0.25, 0.5, 0.75);
|
||||||
|
const TArray<FTransform> GridTransforms2D = UDirectiveUtilMathFunctionLibrary::GenerateGridTransforms2D(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(3, 2), FVector2D(10.0, 20.0), true,
|
||||||
|
GridInstanceRotation, GridInstanceScale);
|
||||||
|
bool bGridTransforms2DValid = GridTransforms2D.Num() == Grid2D.Num();
|
||||||
|
for (int32 Index = 0; Index < GridTransforms2D.Num(); ++Index)
|
||||||
|
{
|
||||||
|
bGridTransforms2DValid &= GridTransforms2D[Index].GetLocation().Equals(Grid2D[Index], 1.e-9);
|
||||||
|
bGridTransforms2DValid &= GridTransforms2D[Index].GetRotation().Equals(GridInstanceQuaternion, 1.e-12);
|
||||||
|
bGridTransforms2DValid &= GridTransforms2D[Index].GetScale3D() == GridInstanceScale;
|
||||||
|
}
|
||||||
|
TestTrue(TEXT("2D grid transforms match point locations and broadcast rotation and scale"),
|
||||||
|
bGridTransforms2DValid);
|
||||||
|
|
||||||
|
const TArray<FTransform> GridTransforms3D = UDirectiveUtilMathFunctionLibrary::GenerateGridTransforms3D(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, FIntVector(2, 2, 2), FVector(2.0, 4.0, 6.0), true,
|
||||||
|
GridInstanceRotation, GridInstanceScale);
|
||||||
|
bool bGridTransforms3DValid = GridTransforms3D.Num() == Grid3D.Num();
|
||||||
|
for (int32 Index = 0; Index < GridTransforms3D.Num(); ++Index)
|
||||||
|
{
|
||||||
|
bGridTransforms3DValid &= GridTransforms3D[Index].GetLocation().Equals(Grid3D[Index], 1.e-9);
|
||||||
|
bGridTransforms3DValid &= GridTransforms3D[Index].GetRotation().Equals(GridInstanceQuaternion, 1.e-12);
|
||||||
|
bGridTransforms3DValid &= GridTransforms3D[Index].GetScale3D() == GridInstanceScale;
|
||||||
|
}
|
||||||
|
TestTrue(TEXT("3D grid transforms match point locations and broadcast rotation and scale"),
|
||||||
|
bGridTransforms3DValid);
|
||||||
|
TestTrue(TEXT("Grid transform generation rejects invalid dimensions and non-finite scale"),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GenerateGridTransforms2D(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(2, 0), FVector2D(1.0), true).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GenerateGridTransforms3D(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, FIntVector(2, 2, 2), FVector::OneVector, true,
|
||||||
|
FRotator::ZeroRotator, FVector(std::numeric_limits<double>::infinity())).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GenerateGridTransforms3D(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, FIntVector(MAX_int32, 2, 2), FVector::OneVector, true)
|
||||||
|
.IsEmpty());
|
||||||
|
|
||||||
|
const double HexRadius = 10.0;
|
||||||
|
const FVector PointyQ = UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation(
|
||||||
|
FIntPoint(1, 0), FVector::ZeroVector, FRotator::ZeroRotator, HexRadius,
|
||||||
|
EDirectiveUtilHexOrientation::PointyTop);
|
||||||
|
const FVector PointyR = UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation(
|
||||||
|
FIntPoint(0, 1), FVector::ZeroVector, FRotator::ZeroRotator, HexRadius,
|
||||||
|
EDirectiveUtilHexOrientation::PointyTop);
|
||||||
|
TestTrue(TEXT("Pointy-top axial coordinates use the expected basis"),
|
||||||
|
PointyQ.Equals(FVector(UE_DOUBLE_SQRT_3 * HexRadius, 0.0, 0.0), 1.e-9)
|
||||||
|
&& PointyR.Equals(FVector(UE_DOUBLE_SQRT_3 * 0.5 * HexRadius, 15.0, 0.0), 1.e-9));
|
||||||
|
|
||||||
|
const FVector FlatQ = UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation(
|
||||||
|
FIntPoint(1, 0), FVector::ZeroVector, FRotator::ZeroRotator, HexRadius,
|
||||||
|
EDirectiveUtilHexOrientation::FlatTop);
|
||||||
|
const FVector FlatR = UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation(
|
||||||
|
FIntPoint(0, 1), FVector::ZeroVector, FRotator::ZeroRotator, HexRadius,
|
||||||
|
EDirectiveUtilHexOrientation::FlatTop);
|
||||||
|
TestTrue(TEXT("Flat-top axial coordinates use the expected basis"),
|
||||||
|
FlatQ.Equals(FVector(15.0, UE_DOUBLE_SQRT_3 * 0.5 * HexRadius, 0.0), 1.e-9)
|
||||||
|
&& FlatR.Equals(FVector(0.0, UE_DOUBLE_SQRT_3 * HexRadius, 0.0), 1.e-9));
|
||||||
|
|
||||||
|
const FVector HexOrigin(11.0, 13.0, 17.0);
|
||||||
|
const FRotator HexRotation(23.0, 37.0, 11.0);
|
||||||
|
const FVector HexPlaneNormal = HexRotation.Quaternion().GetAxisZ();
|
||||||
|
for (const EDirectiveUtilHexOrientation HexOrientation : {
|
||||||
|
EDirectiveUtilHexOrientation::PointyTop, EDirectiveUtilHexOrientation::FlatTop })
|
||||||
|
{
|
||||||
|
const FIntPoint Coordinate(-7, 4);
|
||||||
|
const FVector Location = UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation(
|
||||||
|
Coordinate, HexOrigin, HexRotation, 25.0, HexOrientation, 3.0);
|
||||||
|
TestEqual(TEXT("Hex coordinate conversion round trips through a rotated layout"),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::LocationToHexCoordinate(
|
||||||
|
Location, HexOrigin, HexRotation, 25.0, HexOrientation, 3.0), Coordinate);
|
||||||
|
TestEqual(TEXT("Location conversion projects onto the hex plane"),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::LocationToHexCoordinate(
|
||||||
|
Location + HexPlaneNormal * 500.0, HexOrigin, HexRotation, 25.0, HexOrientation, 3.0), Coordinate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FVector GappedHex = UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation(
|
||||||
|
FIntPoint(1, 0), FVector::ZeroVector, FRotator::ZeroRotator, HexRadius,
|
||||||
|
EDirectiveUtilHexOrientation::PointyTop, 2.0);
|
||||||
|
TestTrue(TEXT("Hex gap adds to the adjacent edge distance"),
|
||||||
|
FMath::IsNearlyEqual(GappedHex.Size(), UE_DOUBLE_SQRT_3 * HexRadius + 2.0, 1.e-9));
|
||||||
|
|
||||||
|
const TArray<FIntPoint> HexNeighbors = UDirectiveUtilMathFunctionLibrary::GetHexNeighbors(FIntPoint(3, -2));
|
||||||
|
const TArray<FIntPoint> ExpectedHexNeighbors = {
|
||||||
|
FIntPoint(4, -2), FIntPoint(4, -3), FIntPoint(3, -3),
|
||||||
|
FIntPoint(2, -2), FIntPoint(2, -1), FIntPoint(3, -1)
|
||||||
|
};
|
||||||
|
TestTrue(TEXT("Hex neighbors use stable axial direction order"), HexNeighbors == ExpectedHexNeighbors);
|
||||||
|
TestEqual(TEXT("Hex distance counts axial grid steps"),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetHexDistance(FIntPoint(0, 0), FIntPoint(3, -5)), 5LL);
|
||||||
|
TestEqual(TEXT("Hex distance uses 64-bit intermediates"),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetHexDistance(
|
||||||
|
FIntPoint(MAX_int32, MAX_int32), FIntPoint(MIN_int32, MIN_int32)), 8589934590LL);
|
||||||
|
|
||||||
|
const TArray<FVector> RectangularHexGrid = UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGrid(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(3, 2), HexRadius,
|
||||||
|
EDirectiveUtilHexOrientation::PointyTop, 0.0, true);
|
||||||
|
TestTrue(TEXT("Rectangular hex grid is centered and ordered by row then column"),
|
||||||
|
RectangularHexGrid.Num() == 6
|
||||||
|
&& RectangularHexGrid[0].Equals(-RectangularHexGrid.Last(), 1.e-9)
|
||||||
|
&& RectangularHexGrid[1].X < RectangularHexGrid[2].X
|
||||||
|
&& RectangularHexGrid[2].Y < RectangularHexGrid[3].Y);
|
||||||
|
const TArray<FVector> UncenteredHexGrid = UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGrid(
|
||||||
|
HexOrigin, FRotator::ZeroRotator, FIntPoint(2, 2), HexRadius,
|
||||||
|
EDirectiveUtilHexOrientation::FlatTop, 0.0, false);
|
||||||
|
TestTrue(TEXT("Uncentered rectangular hex grid starts at its origin"),
|
||||||
|
UncenteredHexGrid.Num() == 4 && UncenteredHexGrid[0] == HexOrigin);
|
||||||
|
|
||||||
|
const TArray<FVector> HexagonalGrid = UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGrid(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, 2, HexRadius,
|
||||||
|
EDirectiveUtilHexOrientation::PointyTop);
|
||||||
|
TSet<FIntPoint> HexagonalCoordinates;
|
||||||
|
bool bHexagonalGridValid = HexagonalGrid.Num() == 19;
|
||||||
|
for (const FVector& Point : HexagonalGrid)
|
||||||
|
{
|
||||||
|
const FIntPoint Coordinate = UDirectiveUtilMathFunctionLibrary::LocationToHexCoordinate(
|
||||||
|
Point, FVector::ZeroVector, FRotator::ZeroRotator, HexRadius,
|
||||||
|
EDirectiveUtilHexOrientation::PointyTop);
|
||||||
|
bHexagonalGridValid &= UDirectiveUtilMathFunctionLibrary::GetHexDistance(FIntPoint::ZeroValue, Coordinate) <= 2;
|
||||||
|
HexagonalCoordinates.Add(Coordinate);
|
||||||
|
}
|
||||||
|
TestTrue(TEXT("Hexagonal grid contains every coordinate through its requested radius"),
|
||||||
|
bHexagonalGridValid && HexagonalCoordinates.Num() == 19);
|
||||||
|
TestEqual(TEXT("A zero-radius hexagonal grid contains its center"),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGrid(
|
||||||
|
HexOrigin, FRotator::ZeroRotator, 0, HexRadius).Num(), 1);
|
||||||
|
TestTrue(TEXT("Hex generators reject invalid layouts and counts"),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGrid(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(0, 2), HexRadius).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGrid(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(2, 2), 0.0).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGrid(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, -1, HexRadius).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGrid(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, 30000, HexRadius).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation(
|
||||||
|
FIntPoint(1, 0), FVector::ZeroVector, FRotator::ZeroRotator, HexRadius,
|
||||||
|
EDirectiveUtilHexOrientation::PointyTop, -UE_DOUBLE_SQRT_3 * HexRadius).IsZero()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GetHexNeighbors(FIntPoint(MAX_int32, 0)).IsEmpty());
|
||||||
|
|
||||||
|
const TArray<FIntPoint> HexesInRange = UDirectiveUtilMathFunctionLibrary::GetHexesInRange(FIntPoint(2, -1), 1);
|
||||||
|
const TArray<FIntPoint> ExpectedHexesInRange = {
|
||||||
|
FIntPoint(2, -2), FIntPoint(3, -2), FIntPoint(1, -1), FIntPoint(2, -1),
|
||||||
|
FIntPoint(3, -1), FIntPoint(1, 0), FIntPoint(2, 0)
|
||||||
|
};
|
||||||
|
TestTrue(TEXT("Hexes in range cover the center and its neighbors ordered by R then Q"),
|
||||||
|
HexesInRange == ExpectedHexesInRange);
|
||||||
|
const TArray<FIntPoint> HexagonalGridCoordinates =
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetHexesInRange(FIntPoint::ZeroValue, 2);
|
||||||
|
bool bHexagonalOrderValid = HexagonalGridCoordinates.Num() == HexagonalGrid.Num();
|
||||||
|
for (int32 Index = 0; bHexagonalOrderValid && Index < HexagonalGrid.Num(); ++Index)
|
||||||
|
{
|
||||||
|
bHexagonalOrderValid &= HexagonalGrid[Index].Equals(
|
||||||
|
UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation(
|
||||||
|
HexagonalGridCoordinates[Index], FVector::ZeroVector, FRotator::ZeroRotator, HexRadius,
|
||||||
|
EDirectiveUtilHexOrientation::PointyTop), 1.e-9);
|
||||||
|
}
|
||||||
|
TestTrue(TEXT("Hexes in range around zero pair with hexagonal grid cells by index"), bHexagonalOrderValid);
|
||||||
|
|
||||||
|
const TArray<FIntPoint> RectangularCoordinates =
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetRectangularHexGridCoordinates(
|
||||||
|
FIntPoint(2, 2), EDirectiveUtilHexOrientation::FlatTop);
|
||||||
|
bool bRectangularOrderValid = RectangularCoordinates.Num() == UncenteredHexGrid.Num();
|
||||||
|
for (int32 Index = 0; bRectangularOrderValid && Index < UncenteredHexGrid.Num(); ++Index)
|
||||||
|
{
|
||||||
|
bRectangularOrderValid &= UncenteredHexGrid[Index].Equals(
|
||||||
|
UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation(
|
||||||
|
RectangularCoordinates[Index], HexOrigin, FRotator::ZeroRotator, HexRadius,
|
||||||
|
EDirectiveUtilHexOrientation::FlatTop), 1.e-9);
|
||||||
|
}
|
||||||
|
TestTrue(TEXT("Rectangular hex grid coordinates pair with grid cells by index"), bRectangularOrderValid);
|
||||||
|
|
||||||
|
const TArray<FIntPoint> HexRing = UDirectiveUtilMathFunctionLibrary::GetHexRing(FIntPoint(1, 1), 2);
|
||||||
|
bool bRingValid = HexRing.Num() == 12;
|
||||||
|
for (int32 Index = 0; bRingValid && Index < HexRing.Num(); ++Index)
|
||||||
|
{
|
||||||
|
bRingValid &= UDirectiveUtilMathFunctionLibrary::GetHexDistance(FIntPoint(1, 1), HexRing[Index]) == 2;
|
||||||
|
bRingValid &= UDirectiveUtilMathFunctionLibrary::GetHexDistance(
|
||||||
|
HexRing[Index], HexRing[(Index + 1) % HexRing.Num()]) == 1;
|
||||||
|
}
|
||||||
|
TestTrue(TEXT("A hex ring traces adjacent cells at the requested radius"), bRingValid);
|
||||||
|
const TArray<FIntPoint> ZeroHexRing = UDirectiveUtilMathFunctionLibrary::GetHexRing(FIntPoint(4, 5), 0);
|
||||||
|
TestTrue(TEXT("A zero-radius hex ring returns the center"),
|
||||||
|
ZeroHexRing.Num() == 1 && ZeroHexRing[0] == FIntPoint(4, 5));
|
||||||
|
|
||||||
|
const TArray<FIntPoint> HexLine = UDirectiveUtilMathFunctionLibrary::GetHexLine(
|
||||||
|
FIntPoint(0, 0), FIntPoint(3, -3));
|
||||||
|
const TArray<FIntPoint> ExpectedHexLine = {
|
||||||
|
FIntPoint(0, 0), FIntPoint(1, -1), FIntPoint(2, -2), FIntPoint(3, -3)
|
||||||
|
};
|
||||||
|
TestTrue(TEXT("A hex line follows a straight axial direction"), HexLine == ExpectedHexLine);
|
||||||
|
const TArray<FIntPoint> DiagonalHexLine = UDirectiveUtilMathFunctionLibrary::GetHexLine(
|
||||||
|
FIntPoint(-1, 2), FIntPoint(1, 3));
|
||||||
|
bool bDiagonalLineValid = DiagonalHexLine.Num() == 4
|
||||||
|
&& DiagonalHexLine[0] == FIntPoint(-1, 2) && DiagonalHexLine.Last() == FIntPoint(1, 3);
|
||||||
|
for (int32 Index = 0; bDiagonalLineValid && Index < DiagonalHexLine.Num() - 1; ++Index)
|
||||||
|
{
|
||||||
|
bDiagonalLineValid &= UDirectiveUtilMathFunctionLibrary::GetHexDistance(
|
||||||
|
DiagonalHexLine[Index], DiagonalHexLine[Index + 1]) == 1;
|
||||||
|
}
|
||||||
|
TestTrue(TEXT("A hex line steps through adjacent cells between its endpoints"), bDiagonalLineValid);
|
||||||
|
const TArray<FIntPoint> SingleHexLine = UDirectiveUtilMathFunctionLibrary::GetHexLine(
|
||||||
|
FIntPoint(7, -2), FIntPoint(7, -2));
|
||||||
|
TestTrue(TEXT("A zero-length hex line returns its cell"),
|
||||||
|
SingleHexLine.Num() == 1 && SingleHexLine[0] == FIntPoint(7, -2));
|
||||||
|
|
||||||
|
const TArray<FVector> HexCorners = UDirectiveUtilMathFunctionLibrary::GetHexCellCorners(
|
||||||
|
FIntPoint::ZeroValue, FVector::ZeroVector, FRotator::ZeroRotator, HexRadius,
|
||||||
|
EDirectiveUtilHexOrientation::PointyTop);
|
||||||
|
bool bCornersValid = HexCorners.Num() == 6
|
||||||
|
&& HexCorners[0].Equals(FVector(UE_DOUBLE_SQRT_3 * 0.5 * HexRadius, 0.5 * HexRadius, 0.0), 1.e-9);
|
||||||
|
for (int32 Index = 0; bCornersValid && Index < 6; ++Index)
|
||||||
|
{
|
||||||
|
bCornersValid &= FMath::IsNearlyEqual(HexCorners[Index].Size(), HexRadius, 1.e-9);
|
||||||
|
bCornersValid &= FMath::IsNearlyEqual(
|
||||||
|
FVector::Distance(HexCorners[Index], HexCorners[(Index + 1) % 6]), HexRadius, 1.e-9);
|
||||||
|
}
|
||||||
|
TestTrue(TEXT("Pointy-top cell corners lie at the cell radius with matching side length"), bCornersValid);
|
||||||
|
const TArray<FVector> FlatHexCorners = UDirectiveUtilMathFunctionLibrary::GetHexCellCorners(
|
||||||
|
FIntPoint::ZeroValue, FVector::ZeroVector, FRotator::ZeroRotator, HexRadius,
|
||||||
|
EDirectiveUtilHexOrientation::FlatTop);
|
||||||
|
TestTrue(TEXT("Flat-top cell corners start on the local X axis"),
|
||||||
|
FlatHexCorners.Num() == 6 && FlatHexCorners[0].Equals(FVector(HexRadius, 0.0, 0.0), 1.e-9));
|
||||||
|
const TArray<FVector> GappedHexCorners = UDirectiveUtilMathFunctionLibrary::GetHexCellCorners(
|
||||||
|
FIntPoint(1, 0), FVector::ZeroVector, FRotator::ZeroRotator, HexRadius,
|
||||||
|
EDirectiveUtilHexOrientation::PointyTop, 2.0);
|
||||||
|
TestTrue(TEXT("Hex gap moves the cell center but not the corner distance"),
|
||||||
|
GappedHexCorners.Num() == 6
|
||||||
|
&& FMath::IsNearlyEqual(FVector::Distance(GappedHexCorners[0], GappedHex), HexRadius, 1.e-9));
|
||||||
|
|
||||||
|
TestTrue(TEXT("Hex queries reject invalid input"),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetHexesInRange(FIntPoint::ZeroValue, -1).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GetHexesInRange(FIntPoint(MAX_int32, 0), 1).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GetHexRing(FIntPoint::ZeroValue, -1).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GetHexRing(FIntPoint(MAX_int32, 0), 1).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GetHexLine(
|
||||||
|
FIntPoint(MIN_int32, MIN_int32), FIntPoint(MAX_int32, MAX_int32)).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GetHexCellCorners(FIntPoint(1, 1), FVector::ZeroVector,
|
||||||
|
FRotator::ZeroRotator, 0.0).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GetRectangularHexGridCoordinates(FIntPoint(0, 3)).IsEmpty());
|
||||||
|
|
||||||
|
const int32 MaximumGeneratedElementCount = UDirectiveUtilMathFunctionLibrary::MaximumGeneratedElementCount;
|
||||||
|
TestEqual(
|
||||||
|
TEXT("The maximum supported rectangular grid size remains available"),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GetRectangularHexGridCoordinates(FIntPoint(1000, 1000)).Num(),
|
||||||
|
MaximumGeneratedElementCount);
|
||||||
|
TestTrue(TEXT("Generated collections reject the first unsupported count"),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GenerateGridPoints2D(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator,
|
||||||
|
FIntPoint(MaximumGeneratedElementCount + 1, 1), FVector2D(1.0, 1.0)).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GetHexesInRange(FIntPoint::ZeroValue, 577).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GetHexRing(
|
||||||
|
FIntPoint::ZeroValue, MaximumGeneratedElementCount / 6 + 1).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GetHexLine(
|
||||||
|
FIntPoint::ZeroValue, FIntPoint(MaximumGeneratedElementCount, 0)).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection(
|
||||||
|
FVector::ZeroVector, FVector::ForwardVector,
|
||||||
|
MaximumGeneratedElementCount + 1, 1.0).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, 1.0,
|
||||||
|
MaximumGeneratedElementCount + 1).IsEmpty());
|
||||||
|
|
||||||
|
const TArray<FVector> NoiseBase = {
|
||||||
|
FVector::ZeroVector, FVector(37.0, 11.0, 5.0), FVector(250.0, -90.0, 40.0)
|
||||||
|
};
|
||||||
|
const TArray<FVector> NoisePoints = UDirectiveUtilMathFunctionLibrary::OffsetLocationsByNoise(
|
||||||
|
NoiseBase, 100.0, 25.0);
|
||||||
|
bool bNoiseValid = NoisePoints.Num() == 3;
|
||||||
|
bool bAnyNoiseOffset = false;
|
||||||
|
for (int32 Index = 0; bNoiseValid && Index < NoisePoints.Num(); ++Index)
|
||||||
|
{
|
||||||
|
const FVector NoiseDelta = NoisePoints[Index] - NoiseBase[Index];
|
||||||
|
bNoiseValid &= FMath::IsNearlyZero(NoiseDelta.X) && FMath::IsNearlyZero(NoiseDelta.Y)
|
||||||
|
&& FMath::Abs(NoiseDelta.Z) <= 25.0 + 1.e-6;
|
||||||
|
bAnyNoiseOffset |= !FMath::IsNearlyZero(NoiseDelta.Z);
|
||||||
|
}
|
||||||
|
TestTrue(TEXT("Noise offsets displace along the requested direction within the amplitude"),
|
||||||
|
bNoiseValid && bAnyNoiseOffset);
|
||||||
|
TestTrue(TEXT("Noise offsets are deterministic"),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::OffsetLocationsByNoise(NoiseBase, 100.0, 25.0) == NoisePoints);
|
||||||
|
TestTrue(TEXT("A zero noise amplitude leaves locations unchanged"),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::OffsetLocationsByNoise(NoiseBase, 100.0, 0.0) == NoiseBase);
|
||||||
|
TestTrue(TEXT("Noise offsets reject invalid input"),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::OffsetLocationsByNoise(NoiseBase, 0.0, 25.0).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::OffsetLocationsByNoise(
|
||||||
|
NoiseBase, 100.0, 25.0, FVector::ZeroVector).IsEmpty());
|
||||||
|
|
||||||
|
const TArray<FVector> DirectionPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection(
|
||||||
|
FVector::ZeroVector, FVector(10.0, 0.0, 0.0), 4, 2.0, true);
|
||||||
|
const TArray<FVector> ExpectedDirectionPoints = {
|
||||||
|
FVector(-3.0, 0.0, 0.0), FVector(-1.0, 0.0, 0.0),
|
||||||
|
FVector(1.0, 0.0, 0.0), FVector(3.0, 0.0, 0.0)
|
||||||
|
};
|
||||||
|
TestTrue(TEXT("Direction points normalize once and center around the origin"),
|
||||||
|
PointsEqual(DirectionPoints, ExpectedDirectionPoints));
|
||||||
|
const TArray<FVector> ReversedDirectionPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection(
|
||||||
|
FVector::ZeroVector, FVector::ForwardVector, 3, -2.0, false);
|
||||||
|
TestTrue(TEXT("Direction points preserve signed spacing"),
|
||||||
|
ReversedDirectionPoints.Num() == 3
|
||||||
|
&& ReversedDirectionPoints[0].Equals(FVector::ZeroVector)
|
||||||
|
&& ReversedDirectionPoints[2].Equals(FVector(-4.0, 0.0, 0.0)));
|
||||||
|
TestTrue(TEXT("Direction points reject a zero direction"),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection(
|
||||||
|
FVector::ZeroVector, FVector::ZeroVector, 3, 1.0, false).IsEmpty());
|
||||||
|
|
||||||
|
const TArray<FVector> SegmentPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsBetweenLocations(
|
||||||
|
FVector::ZeroVector, FVector(10.0, 0.0, 0.0), 3, true);
|
||||||
|
TestTrue(TEXT("Segment points include exact endpoints"),
|
||||||
|
SegmentPoints.Num() == 3
|
||||||
|
&& SegmentPoints[0] == FVector::ZeroVector
|
||||||
|
&& SegmentPoints[1].Equals(FVector(5.0, 0.0, 0.0))
|
||||||
|
&& SegmentPoints[2] == FVector(10.0, 0.0, 0.0));
|
||||||
|
const TArray<FVector> InteriorSegmentPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsBetweenLocations(
|
||||||
|
FVector::ZeroVector, FVector(9.0, 0.0, 0.0), 2, false);
|
||||||
|
TestTrue(TEXT("Segment points can exclude both endpoints"),
|
||||||
|
InteriorSegmentPoints.Num() == 2
|
||||||
|
&& InteriorSegmentPoints[0].Equals(FVector(3.0, 0.0, 0.0))
|
||||||
|
&& InteriorSegmentPoints[1].Equals(FVector(6.0, 0.0, 0.0)));
|
||||||
|
const TArray<FVector> SingleSegmentPoint = UDirectiveUtilMathFunctionLibrary::GeneratePointsBetweenLocations(
|
||||||
|
FVector(2.0, 4.0, 6.0), FVector(6.0, 8.0, 10.0), 1, true);
|
||||||
|
TestTrue(TEXT("A single segment point is the midpoint"),
|
||||||
|
SingleSegmentPoint.Num() == 1 && SingleSegmentPoint[0].Equals(FVector(4.0, 6.0, 8.0)));
|
||||||
|
|
||||||
|
USplineComponent* Spline = NewObject<USplineComponent>();
|
||||||
|
Spline->SetSplinePoints({ FVector::ZeroVector, FVector(100.0, 0.0, 0.0) },
|
||||||
|
ESplineCoordinateSpace::Local, false);
|
||||||
|
Spline->SetSplinePointType(0, ESplinePointType::Linear, false);
|
||||||
|
Spline->SetSplinePointType(1, ESplinePointType::Linear, true);
|
||||||
|
TestTrue(TEXT("Spline generators reject unsupported sample counts"),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSplineByCount(
|
||||||
|
Spline, MaximumGeneratedElementCount + 1).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSplineByCount(
|
||||||
|
Spline, MaximumGeneratedElementCount + 1).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||||
|
Spline, 0.00005).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSpline(
|
||||||
|
Spline, 0.00005).IsEmpty());
|
||||||
|
const TArray<FVector> SplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||||
|
Spline, 30.0, true, EDirectiveUtilSplineSpacingMode::Fixed);
|
||||||
|
TestTrue(TEXT("Spline points use fixed spacing and append the exact open endpoint"),
|
||||||
|
SplinePoints.Num() == 5
|
||||||
|
&& SplinePoints[0].Equals(FVector::ZeroVector)
|
||||||
|
&& SplinePoints[1].Equals(FVector(30.0, 0.0, 0.0), 1.e-4)
|
||||||
|
&& SplinePoints[3].Equals(FVector(90.0, 0.0, 0.0), 1.e-4)
|
||||||
|
&& SplinePoints[4].Equals(FVector(100.0, 0.0, 0.0), 1.e-4));
|
||||||
|
TestEqual(TEXT("Spline endpoint can be excluded"),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||||
|
Spline, 30.0, false, EDirectiveUtilSplineSpacingMode::Fixed).Num(), 4);
|
||||||
|
const TArray<FVector> EvenSplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||||
|
Spline, 30.0, true, EDirectiveUtilSplineSpacingMode::Even);
|
||||||
|
TestTrue(TEXT("Even spline spacing divides the range without a short final interval"),
|
||||||
|
EvenSplinePoints.Num() == 5
|
||||||
|
&& EvenSplinePoints[1].Equals(FVector(25.0, 0.0, 0.0), 1.e-4)
|
||||||
|
&& EvenSplinePoints[3].Equals(FVector(75.0, 0.0, 0.0), 1.e-4)
|
||||||
|
&& EvenSplinePoints[4].Equals(FVector(100.0, 0.0, 0.0), 1.e-4));
|
||||||
|
const TArray<FVector> RangedSplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||||
|
Spline, 25.0, true, EDirectiveUtilSplineSpacingMode::Fixed, ESplineCoordinateSpace::World, 20.0, 80.0);
|
||||||
|
TestTrue(TEXT("Spline sampling honors a start and end distance"),
|
||||||
|
RangedSplinePoints.Num() == 4
|
||||||
|
&& RangedSplinePoints[0].Equals(FVector(20.0, 0.0, 0.0), 1.e-4)
|
||||||
|
&& RangedSplinePoints[1].Equals(FVector(45.0, 0.0, 0.0), 1.e-4)
|
||||||
|
&& RangedSplinePoints[3].Equals(FVector(80.0, 0.0, 0.0), 1.e-4));
|
||||||
|
TestEqual(TEXT("Spline sampling drops a regular sample that lands on the endpoint"),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||||
|
Spline, 25.0, true, EDirectiveUtilSplineSpacingMode::Fixed, ESplineCoordinateSpace::World,
|
||||||
|
0.0, 50.0 + 1.e-10).Num(), 3);
|
||||||
|
const TArray<FVector> CountedSplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSplineByCount(
|
||||||
|
Spline, 5, true);
|
||||||
|
TestTrue(TEXT("Spline sampling by count includes both exact endpoints"),
|
||||||
|
CountedSplinePoints.Num() == 5
|
||||||
|
&& CountedSplinePoints[0].Equals(FVector::ZeroVector, 1.e-4)
|
||||||
|
&& CountedSplinePoints[2].Equals(FVector(50.0, 0.0, 0.0), 1.e-4)
|
||||||
|
&& CountedSplinePoints[4].Equals(FVector(100.0, 0.0, 0.0), 1.e-4));
|
||||||
|
const TArray<FVector> InteriorSplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSplineByCount(
|
||||||
|
Spline, 4, false);
|
||||||
|
TestTrue(TEXT("Spline sampling by count can exclude both endpoints"),
|
||||||
|
InteriorSplinePoints.Num() == 4
|
||||||
|
&& InteriorSplinePoints[0].Equals(FVector(20.0, 0.0, 0.0), 1.e-4)
|
||||||
|
&& InteriorSplinePoints[3].Equals(FVector(80.0, 0.0, 0.0), 1.e-4));
|
||||||
|
const TArray<FVector> SingleSplinePoint = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSplineByCount(
|
||||||
|
Spline, 1, true);
|
||||||
|
TestTrue(TEXT("A single spline point by count is the range midpoint"),
|
||||||
|
SingleSplinePoint.Num() == 1 && SingleSplinePoint[0].Equals(FVector(50.0, 0.0, 0.0), 1.e-4));
|
||||||
|
Spline->SetClosedLoop(true, true);
|
||||||
|
const TArray<FVector> ClosedSplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||||
|
Spline, 30.0, true, EDirectiveUtilSplineSpacingMode::Fixed);
|
||||||
|
TestTrue(TEXT("Closed spline sampling does not repeat its first point"),
|
||||||
|
ClosedSplinePoints.Num() > 1 && !ClosedSplinePoints[0].Equals(ClosedSplinePoints.Last(), 1.e-4));
|
||||||
|
const TArray<FVector> ClosedCountedPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSplineByCount(
|
||||||
|
Spline, 4, true);
|
||||||
|
TestTrue(TEXT("Closed spline sampling by count spreads points around the loop"),
|
||||||
|
ClosedCountedPoints.Num() == 4
|
||||||
|
&& !ClosedCountedPoints[0].Equals(ClosedCountedPoints.Last(), 1.e-4));
|
||||||
|
USplineComponent* SinglePointSpline = NewObject<USplineComponent>();
|
||||||
|
SinglePointSpline->SetSplinePoints({ FVector(3.0, 4.0, 5.0) }, ESplineCoordinateSpace::Local, true);
|
||||||
|
const TArray<FVector> ZeroLengthSplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||||
|
SinglePointSpline, 10.0);
|
||||||
|
TestTrue(TEXT("A zero-length spline returns its only point"),
|
||||||
|
ZeroLengthSplinePoints.Num() == 1 && ZeroLengthSplinePoints[0].Equals(FVector(3.0, 4.0, 5.0)));
|
||||||
|
TestTrue(TEXT("Spline sampling rejects invalid input"),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(nullptr, 10.0).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(Spline, 0.0).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||||
|
Spline, 10.0, true, static_cast<EDirectiveUtilSplineSpacingMode>(255)).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||||
|
Spline, 10.0, true, EDirectiveUtilSplineSpacingMode::Fixed,
|
||||||
|
static_cast<ESplineCoordinateSpace::Type>(255)).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||||
|
Spline, 10.0, true, EDirectiveUtilSplineSpacingMode::Fixed,
|
||||||
|
ESplineCoordinateSpace::World, 80.0, 20.0).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSplineByCount(Spline, 0).IsEmpty());
|
||||||
|
|
||||||
|
const TArray<FVector> CirclePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, 10.0, 4, 0.0);
|
||||||
|
const TArray<FVector> ExpectedCirclePoints = {
|
||||||
|
FVector(10.0, 0.0, 0.0), FVector(0.0, 10.0, 0.0),
|
||||||
|
FVector(-10.0, 0.0, 0.0), FVector(0.0, -10.0, 0.0)
|
||||||
|
};
|
||||||
|
TestTrue(TEXT("Circle points are evenly spaced without repeating the first point"),
|
||||||
|
PointsEqual(CirclePoints, ExpectedCirclePoints, 1.e-8));
|
||||||
|
|
||||||
|
const FRotator PlaneRotation(17.0, 31.0, 43.0);
|
||||||
|
const FVector PlaneNormal = PlaneRotation.Quaternion().GetAxisZ();
|
||||||
|
const FVector PlaneCenter(11.0, 13.0, 17.0);
|
||||||
|
const TArray<FVector> RotatedCircle = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle(
|
||||||
|
PlaneCenter, PlaneRotation, -7.0, 64, 15.0);
|
||||||
|
bool bCirclePlaneValid = RotatedCircle.Num() == 64;
|
||||||
|
for (const FVector& Point : RotatedCircle)
|
||||||
|
{
|
||||||
|
const FVector Offset = Point - PlaneCenter;
|
||||||
|
bCirclePlaneValid &= FMath::IsNearlyEqual(Offset.Size(), 7.0, 1.e-8);
|
||||||
|
bCirclePlaneValid &= FMath::IsNearlyZero(FVector::DotProduct(Offset, PlaneNormal), 1.e-8);
|
||||||
|
}
|
||||||
|
TestTrue(TEXT("Circle points honor rotation and negative radius"), bCirclePlaneValid);
|
||||||
|
|
||||||
|
const TArray<FVector> ArcPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnArc(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, 10.0, 3, 0.0, 90.0, true);
|
||||||
|
TestTrue(TEXT("Arc points include the requested endpoint"),
|
||||||
|
ArcPoints.Num() == 3
|
||||||
|
&& ArcPoints[0].Equals(FVector(10.0, 0.0, 0.0), 1.e-8)
|
||||||
|
&& ArcPoints[1].Equals(FVector(UE_DOUBLE_INV_SQRT_2 * 10.0, UE_DOUBLE_INV_SQRT_2 * 10.0, 0.0), 1.e-8)
|
||||||
|
&& ArcPoints[2].Equals(FVector(0.0, 10.0, 0.0), 1.e-8));
|
||||||
|
const TArray<FVector> OpenArcPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnArc(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, 10.0, 3, 0.0, 90.0, false);
|
||||||
|
TestTrue(TEXT("Open arc points exclude the requested endpoint"),
|
||||||
|
OpenArcPoints.Num() == 3
|
||||||
|
&& OpenArcPoints.Last().Equals(
|
||||||
|
FVector(FMath::Cos(UE_DOUBLE_PI / 3.0) * 10.0, FMath::Sin(UE_DOUBLE_PI / 3.0) * 10.0, 0.0), 1.e-8));
|
||||||
|
|
||||||
|
const TArray<FVector> DiscPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnDisc(
|
||||||
|
PlaneCenter, PlaneRotation, 25.0, 1024, 27.0);
|
||||||
|
const TArray<FVector> RepeatedDiscPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnDisc(
|
||||||
|
PlaneCenter, PlaneRotation, 25.0, 1024, 27.0);
|
||||||
|
bool bDiscValid = DiscPoints.Num() == 1024;
|
||||||
|
for (const FVector& Point : DiscPoints)
|
||||||
|
{
|
||||||
|
const FVector Offset = Point - PlaneCenter;
|
||||||
|
bDiscValid &= Offset.Size() < 25.0;
|
||||||
|
bDiscValid &= FMath::IsNearlyZero(FVector::DotProduct(Offset, PlaneNormal), 1.e-8);
|
||||||
|
}
|
||||||
|
TestTrue(TEXT("Disc points are deterministic and remain inside the rotated disc"),
|
||||||
|
bDiscValid && PointsEqual(DiscPoints, RepeatedDiscPoints));
|
||||||
|
const TArray<FVector> SingleDiscPoint = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnDisc(
|
||||||
|
PlaneCenter, PlaneRotation, 25.0, 1, 27.0);
|
||||||
|
TestTrue(TEXT("A single disc point is its center"),
|
||||||
|
SingleDiscPoint.Num() == 1 && SingleDiscPoint[0] == PlaneCenter);
|
||||||
|
|
||||||
|
const TArray<FVector> SpherePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
|
||||||
|
FVector::ZeroVector, PlaneRotation, 25.0, 1024, 27.0);
|
||||||
|
const TArray<FVector> RepeatedSpherePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
|
||||||
|
FVector::ZeroVector, PlaneRotation, 25.0, 1024, 27.0);
|
||||||
|
FVector SphereMean = FVector::ZeroVector;
|
||||||
|
bool bSphereValid = SpherePoints.Num() == 1024;
|
||||||
|
for (const FVector& Point : SpherePoints)
|
||||||
|
{
|
||||||
|
bSphereValid &= FMath::IsNearlyEqual(Point.Size(), 25.0, 1.e-8);
|
||||||
|
SphereMean += Point;
|
||||||
|
}
|
||||||
|
if (!SpherePoints.IsEmpty())
|
||||||
|
{
|
||||||
|
SphereMean /= SpherePoints.Num();
|
||||||
|
}
|
||||||
|
TestTrue(TEXT("Sphere points are deterministic and remain on the surface"),
|
||||||
|
bSphereValid && SphereMean.Size() < 0.01 && PointsEqual(SpherePoints, RepeatedSpherePoints));
|
||||||
|
|
||||||
|
const FVector SphereCenter(-1200.0, 3400.0, -5600.0);
|
||||||
|
const FRotator SphereRotation(-37.0, 123.0, 71.0);
|
||||||
|
const FQuat SphereRotationQuaternion = SphereRotation.Quaternion();
|
||||||
|
const TArray<FVector> LocalSpherePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, 17.0, 257, 23.5);
|
||||||
|
const TArray<FVector> RotatedSpherePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
|
||||||
|
SphereCenter, SphereRotation, 17.0, 257, 23.5);
|
||||||
|
bool bSphereRotationValid = LocalSpherePoints.Num() == RotatedSpherePoints.Num();
|
||||||
|
for (int32 Index = 0; Index < LocalSpherePoints.Num() && bSphereRotationValid; ++Index)
|
||||||
|
{
|
||||||
|
const FVector ExpectedPoint = SphereCenter
|
||||||
|
+ SphereRotationQuaternion.RotateVector(LocalSpherePoints[Index]);
|
||||||
|
bSphereRotationValid &= RotatedSpherePoints[Index].Equals(ExpectedPoint, 1.e-8);
|
||||||
|
}
|
||||||
|
TestTrue(TEXT("Sphere rotation transforms every local distribution point"), bSphereRotationValid);
|
||||||
|
|
||||||
|
constexpr double SphereAngleOffset = 1153.25;
|
||||||
|
const TArray<FVector> UnoffsetSpherePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, 17.0, 257, 0.0);
|
||||||
|
const TArray<FVector> OffsetSpherePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, 17.0, 257, SphereAngleOffset);
|
||||||
|
const FQuat SphereOffsetRotation(FVector::UpVector,
|
||||||
|
FMath::DegreesToRadians(FMath::Fmod(SphereAngleOffset, 360.0)));
|
||||||
|
bool bSphereAngleOffsetValid = UnoffsetSpherePoints.Num() == OffsetSpherePoints.Num();
|
||||||
|
for (int32 Index = 0; Index < UnoffsetSpherePoints.Num() && bSphereAngleOffsetValid; ++Index)
|
||||||
|
{
|
||||||
|
bSphereAngleOffsetValid &= OffsetSpherePoints[Index].Equals(
|
||||||
|
SphereOffsetRotation.RotateVector(UnoffsetSpherePoints[Index]), 1.e-8);
|
||||||
|
}
|
||||||
|
TestTrue(TEXT("Sphere angle offset rotates the distribution around local Z"),
|
||||||
|
bSphereAngleOffsetValid);
|
||||||
|
|
||||||
|
const TArray<FVector> SingleSpherePoint = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
|
||||||
|
PlaneCenter, PlaneRotation, 25.0, 1, 27.0);
|
||||||
|
TestTrue(TEXT("A single sphere point follows the rotated local Z axis"),
|
||||||
|
SingleSpherePoint.Num() == 1
|
||||||
|
&& SingleSpherePoint[0].Equals(PlaneCenter + PlaneNormal * 25.0, 1.e-8));
|
||||||
|
|
||||||
|
const FVector LargeTranslation(1000000.0, -2000000.0, 3000000.0);
|
||||||
|
const FRotator AuditRotation(-37.0, 123.0, 71.0);
|
||||||
|
TestTrue(TEXT("A translated center offsets every generated arc point exactly once"),
|
||||||
|
PointsEqualAfterTranslation(
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GeneratePointsOnArc(
|
||||||
|
FVector::ZeroVector, AuditRotation, -13.5, 11, 1080.25, -450.5, true),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GeneratePointsOnArc(
|
||||||
|
LargeTranslation, AuditRotation, -13.5, 11, 1080.25, -450.5, true),
|
||||||
|
LargeTranslation));
|
||||||
|
|
||||||
|
TestTrue(TEXT("Translated origins preserve every spatial generator's local offsets"),
|
||||||
|
PointsEqualAfterTranslation(
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GenerateGridPoints2D(
|
||||||
|
FVector::ZeroVector, AuditRotation, FIntPoint(4, 3), FVector2D(-7.0, 11.0), true),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GenerateGridPoints2D(
|
||||||
|
LargeTranslation, AuditRotation, FIntPoint(4, 3), FVector2D(-7.0, 11.0), true),
|
||||||
|
LargeTranslation)
|
||||||
|
&& PointsEqualAfterTranslation(
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GenerateGridPoints3D(
|
||||||
|
FVector::ZeroVector, AuditRotation, FIntVector(3, 2, 2), FVector(5.0, -7.0, 0.0), false),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GenerateGridPoints3D(
|
||||||
|
LargeTranslation, AuditRotation, FIntVector(3, 2, 2), FVector(5.0, -7.0, 0.0), false),
|
||||||
|
LargeTranslation)
|
||||||
|
&& PointsEqualAfterTranslation(
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection(
|
||||||
|
FVector::ZeroVector, FVector(-2.0, 3.0, -5.0), 7, -3.25, true),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection(
|
||||||
|
LargeTranslation, FVector(-2.0, 3.0, -5.0), 7, -3.25, true),
|
||||||
|
LargeTranslation)
|
||||||
|
&& PointsEqualAfterTranslation(
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GeneratePointsBetweenLocations(
|
||||||
|
FVector(-11.0, 5.0, 8.0), FVector(17.0, -9.0, 3.0), 8, false),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GeneratePointsBetweenLocations(
|
||||||
|
LargeTranslation + FVector(-11.0, 5.0, 8.0),
|
||||||
|
LargeTranslation + FVector(17.0, -9.0, 3.0), 8, false),
|
||||||
|
LargeTranslation)
|
||||||
|
&& PointsEqualAfterTranslation(
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle(
|
||||||
|
FVector::ZeroVector, AuditRotation, -13.5, 17, -725.25),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle(
|
||||||
|
LargeTranslation, AuditRotation, -13.5, 17, -725.25),
|
||||||
|
LargeTranslation)
|
||||||
|
&& PointsEqualAfterTranslation(
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GeneratePointsOnDisc(
|
||||||
|
FVector::ZeroVector, AuditRotation, -13.5, 257, 1080.25),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GeneratePointsOnDisc(
|
||||||
|
LargeTranslation, AuditRotation, -13.5, 257, 1080.25),
|
||||||
|
LargeTranslation)
|
||||||
|
&& PointsEqualAfterTranslation(
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
|
||||||
|
FVector::ZeroVector, AuditRotation, -13.5, 257, -1080.25),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
|
||||||
|
LargeTranslation, AuditRotation, -13.5, 257, -1080.25),
|
||||||
|
LargeTranslation));
|
||||||
|
|
||||||
|
for (const EDirectiveUtilHexOrientation Orientation : {
|
||||||
|
EDirectiveUtilHexOrientation::PointyTop, EDirectiveUtilHexOrientation::FlatTop })
|
||||||
|
{
|
||||||
|
TestTrue(TEXT("Translated origins preserve rectangular and hexagonal grid offsets"),
|
||||||
|
PointsEqualAfterTranslation(
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGrid(
|
||||||
|
FVector::ZeroVector, AuditRotation, FIntPoint(4, 3), 9.5, Orientation, -1.25, true),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGrid(
|
||||||
|
LargeTranslation, AuditRotation, FIntPoint(4, 3), 9.5, Orientation, -1.25, true),
|
||||||
|
LargeTranslation)
|
||||||
|
&& PointsEqualAfterTranslation(
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGrid(
|
||||||
|
FVector::ZeroVector, AuditRotation, 4, 9.5, Orientation, -1.25),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGrid(
|
||||||
|
LargeTranslation, AuditRotation, 4, 9.5, Orientation, -1.25),
|
||||||
|
LargeTranslation));
|
||||||
|
|
||||||
|
for (const FIntPoint Coordinate : {
|
||||||
|
FIntPoint::ZeroValue, FIntPoint(-17, 29), FIntPoint(1234, -987), FIntPoint(-4096, -2048) })
|
||||||
|
{
|
||||||
|
const FVector Location = UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation(
|
||||||
|
Coordinate, LargeTranslation, AuditRotation, 9.5, Orientation, -1.25);
|
||||||
|
TestEqual(TEXT("Odd signed hex coordinates round trip through translated rotated layouts"),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::LocationToHexCoordinate(
|
||||||
|
Location, LargeTranslation, AuditRotation, 9.5, Orientation, -1.25), Coordinate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TArray<FVector> ZeroRadiusCircle = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle(
|
||||||
|
LargeTranslation, AuditRotation, 0.0, 9, 123.0);
|
||||||
|
const TArray<FVector> ZeroRadiusArc = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnArc(
|
||||||
|
LargeTranslation, AuditRotation, 0.0, 9, -30.0, -720.0, false);
|
||||||
|
const TArray<FVector> ZeroRadiusDisc = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnDisc(
|
||||||
|
LargeTranslation, AuditRotation, 0.0, 9, 123.0);
|
||||||
|
const TArray<FVector> ZeroRadiusSphere = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
|
||||||
|
LargeTranslation, AuditRotation, 0.0, 9, 123.0);
|
||||||
|
TestTrue(TEXT("Zero-radius generators preserve their requested count at the center"),
|
||||||
|
ZeroRadiusCircle.Num() == 9 && PointsMatchLocation(ZeroRadiusCircle, LargeTranslation)
|
||||||
|
&& ZeroRadiusArc.Num() == 9 && PointsMatchLocation(ZeroRadiusArc, LargeTranslation)
|
||||||
|
&& ZeroRadiusDisc.Num() == 9 && PointsMatchLocation(ZeroRadiusDisc, LargeTranslation)
|
||||||
|
&& ZeroRadiusSphere.Num() == 9 && PointsMatchLocation(ZeroRadiusSphere, LargeTranslation));
|
||||||
|
|
||||||
|
TestTrue(TEXT("Degenerate linear generators preserve their requested count and location"),
|
||||||
|
PointsMatchLocation(UDirectiveUtilMathFunctionLibrary::GenerateGridPoints3D(
|
||||||
|
LargeTranslation, AuditRotation, FIntVector(2, 3, 4), FVector::ZeroVector, true), LargeTranslation)
|
||||||
|
&& PointsMatchLocation(UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection(
|
||||||
|
LargeTranslation, FVector(2.0, -3.0, 4.0), 7, 0.0, true), LargeTranslation)
|
||||||
|
&& PointsMatchLocation(UDirectiveUtilMathFunctionLibrary::GeneratePointsBetweenLocations(
|
||||||
|
LargeTranslation, LargeTranslation, 7, false), LargeTranslation));
|
||||||
|
|
||||||
|
USplineComponent* TransformedSpline = NewObject<USplineComponent>();
|
||||||
|
TransformedSpline->SetWorldLocation(LargeTranslation);
|
||||||
|
TransformedSpline->SetWorldRotation(AuditRotation);
|
||||||
|
TransformedSpline->SetWorldScale3D(FVector(2.0, 3.0, 0.5));
|
||||||
|
TransformedSpline->SetSplinePoints({ FVector::ZeroVector, FVector(100.0, 0.0, 0.0) },
|
||||||
|
ESplineCoordinateSpace::Local, false);
|
||||||
|
TransformedSpline->SetSplinePointType(0, ESplinePointType::Linear, false);
|
||||||
|
TransformedSpline->SetSplinePointType(1, ESplinePointType::Linear, true);
|
||||||
|
const TArray<FVector> TransformedSplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||||
|
TransformedSpline, 60.0);
|
||||||
|
TestEqual(TEXT("Scaled spline sampling produces the expected point count"), TransformedSplinePoints.Num(), 5);
|
||||||
|
if (TransformedSplinePoints.Num() == 5)
|
||||||
|
{
|
||||||
|
TestTrue(TEXT("Spline sampling returns its world-space start"),
|
||||||
|
TransformedSplinePoints[0].Equals(LargeTranslation, 1.e-4));
|
||||||
|
TestTrue(TEXT("Spline sampling preserves regular world-space intervals"),
|
||||||
|
FMath::IsNearlyEqual(FVector::Distance(TransformedSplinePoints[0], TransformedSplinePoints[1]), 60.0, 1.e-4)
|
||||||
|
&& FMath::IsNearlyEqual(FVector::Distance(TransformedSplinePoints[1], TransformedSplinePoints[2]), 60.0, 1.e-4)
|
||||||
|
&& FMath::IsNearlyEqual(FVector::Distance(TransformedSplinePoints[2], TransformedSplinePoints[3]), 60.0, 1.e-4));
|
||||||
|
TestTrue(TEXT("Spline sampling appends its exact world-space endpoint"),
|
||||||
|
TransformedSplinePoints.Last().Equals(TransformedSpline->GetLocationAtDistanceAlongSpline(
|
||||||
|
TransformedSpline->GetSplineLength(), ESplineCoordinateSpace::World), 1.e-4));
|
||||||
|
}
|
||||||
|
const TArray<FVector> LocalSplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||||
|
TransformedSpline, 60.0, true, EDirectiveUtilSplineSpacingMode::Fixed, ESplineCoordinateSpace::Local);
|
||||||
|
TestTrue(TEXT("Spline sampling can return local-space points"),
|
||||||
|
LocalSplinePoints.Num() == TransformedSplinePoints.Num()
|
||||||
|
&& LocalSplinePoints[0].Equals(FVector::ZeroVector, 1.e-4)
|
||||||
|
&& LocalSplinePoints.Last().Equals(FVector(100.0, 0.0, 0.0), 1.e-4));
|
||||||
|
|
||||||
|
const double Infinity = std::numeric_limits<double>::infinity();
|
||||||
|
const double NaN = std::numeric_limits<double>::quiet_NaN();
|
||||||
|
const FRotator InvalidRotation(Infinity, 0.0, 0.0);
|
||||||
|
TestTrue(TEXT("Point generators reject non-finite values"),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GenerateGridPoints2D(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(2, 2), FVector2D(Infinity, 1.0), true).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection(
|
||||||
|
FVector::ZeroVector, FVector::ForwardVector, 2, Infinity, false).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle(
|
||||||
|
FVector::ZeroVector, InvalidRotation, 1.0, 4, 0.0).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsOnArc(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, 1.0, 4, 0.0, Infinity, true).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsOnDisc(
|
||||||
|
FVector(Infinity, 0.0, 0.0), FRotator::ZeroRotator, 1.0, 4, 0.0).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, Infinity, 4, 0.0).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGrid(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(2, 2), Infinity).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(Spline, Infinity).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
|
||||||
|
Spline, 10.0, true, EDirectiveUtilSplineSpacingMode::Fixed,
|
||||||
|
ESplineCoordinateSpace::World, Infinity, -1.0).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSplineByCount(
|
||||||
|
Spline, 2, true, ESplineCoordinateSpace::World, 0.0, NaN).IsEmpty());
|
||||||
|
TestTrue(TEXT("Every spatial generator rejects a non-finite origin or center"),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GenerateGridPoints3D(
|
||||||
|
FVector(NaN, 0.0, 0.0), FRotator::ZeroRotator, FIntVector(1), FVector::OneVector, true).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsBetweenLocations(
|
||||||
|
FVector::ZeroVector, FVector(Infinity, 0.0, 0.0), 2, true).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsOnArc(
|
||||||
|
FVector(NaN, 0.0, 0.0), FRotator::ZeroRotator, 1.0, 2, 0.0, 90.0, true).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGrid(
|
||||||
|
FVector(Infinity, 0.0, 0.0), FRotator::ZeroRotator, 1, 1.0).IsEmpty());
|
||||||
|
TestTrue(TEXT("Spline sampling rejects negative spacing"),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(Spline, -1.0).IsEmpty());
|
||||||
|
TestTrue(TEXT("Point generators return empty arrays for non-positive counts"),
|
||||||
|
UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection(
|
||||||
|
FVector::ZeroVector, FVector::ForwardVector, 0, 1.0, false).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsBetweenLocations(
|
||||||
|
FVector::ZeroVector, FVector::OneVector, -1, true).IsEmpty()
|
||||||
|
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle(
|
||||||
|
FVector::ZeroVector, FRotator::ZeroRotator, 1.0, 0, 0.0).IsEmpty());
|
||||||
|
|
||||||
|
return !HasAnyErrors();
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
#include "Libraries/DirectiveUtilRegexFunctionLibrary.h"
|
#include "Libraries/DirectiveUtilRegexFunctionLibrary.h"
|
||||||
#include "Misc/AutomationTest.h"
|
#include "Misc/AutomationTest.h"
|
||||||
|
|
||||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilRegexFunctionLibraryTest, "DirectiveUtilities.RegexFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilRegexFunctionLibraryTest, "DirectiveUtilities.RegexFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
bool FDirectiveUtilRegexFunctionLibraryTest::RunTest(const FString& Parameters)
|
bool FDirectiveUtilRegexFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||||
{
|
{
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,104 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "Libraries/DirectiveUtilArrayFunctionLibrary.h"
|
||||||
|
#include "Libraries/DirectiveUtilFunctionLibrary.h"
|
||||||
|
#include "Libraries/DirectiveUtilGameplayTagFunctionLibrary.h"
|
||||||
|
#include "Libraries/DirectiveUtilInputFunctionLibrary.h"
|
||||||
|
#include "Libraries/DirectiveUtilMapFunctionLibrary.h"
|
||||||
|
#include "Libraries/DirectiveUtilMathFunctionLibrary.h"
|
||||||
|
#include "Libraries/DirectiveUtilRegexFunctionLibrary.h"
|
||||||
|
#include "Libraries/DirectiveUtilSaveGameFunctionLibrary.h"
|
||||||
|
#include "Libraries/DirectiveUtilStringFunctionLibrary.h"
|
||||||
|
#include "Libraries/DirectiveUtilTextFunctionLibrary.h"
|
||||||
|
#include "Misc/AutomationTest.h"
|
||||||
|
#include "Modules/ModuleManager.h"
|
||||||
|
#include "Tasks/DirectiveUtilTask_AsyncLoadAsset.h"
|
||||||
|
#include "Tasks/DirectiveUtilTask_AsyncTrace.h"
|
||||||
|
#include "Tasks/DirectiveUtilTask_Delay.h"
|
||||||
|
#include "Tasks/DirectiveUtilTask_Flow.h"
|
||||||
|
#include "Tasks/DirectiveUtilTask_MoveToLocation.h"
|
||||||
|
#include "UObject/Class.h"
|
||||||
|
#include "UObject/Package.h"
|
||||||
|
#include "UObject/UnrealType.h"
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
|
FDirectiveUtilRuntimeSurfaceTest,
|
||||||
|
"DirectiveUtilities.Runtime.Surface",
|
||||||
|
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FDirectiveUtilRuntimeSurfaceTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
struct FClassExpectation
|
||||||
|
{
|
||||||
|
UClass* Class;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FClassExpectation Expectations[] = {
|
||||||
|
{ UDirectiveUtilArrayFunctionLibrary::StaticClass() },
|
||||||
|
{ UDirectiveUtilFunctionLibrary::StaticClass() },
|
||||||
|
{ UDirectiveUtilGameplayTagFunctionLibrary::StaticClass() },
|
||||||
|
{ UDirectiveUtilInputFunctionLibrary::StaticClass() },
|
||||||
|
{ UDirectiveUtilMapFunctionLibrary::StaticClass() },
|
||||||
|
{ UDirectiveUtilMathFunctionLibrary::StaticClass() },
|
||||||
|
{ UDirectiveUtilRegexFunctionLibrary::StaticClass() },
|
||||||
|
{ UDirectiveUtilSaveGameFunctionLibrary::StaticClass() },
|
||||||
|
{ UDirectiveUtilStringFunctionLibrary::StaticClass() },
|
||||||
|
{ UDirectiveUtilTextFunctionLibrary::StaticClass() },
|
||||||
|
{ UDirectiveUtilTask_AsyncLoadAsset::StaticClass() },
|
||||||
|
{ UDirectiveUtilTask_AsyncLoadClass::StaticClass() },
|
||||||
|
{ UDirectiveUtilTask_AsyncLoadAssets::StaticClass() },
|
||||||
|
{ UDirectiveUtilTask_AsyncTrace::StaticClass() },
|
||||||
|
{ UDirectiveUtilTask_Delay::StaticClass() },
|
||||||
|
{ UDirectiveUtilTask_UpdateForDuration::StaticClass() },
|
||||||
|
{ UDirectiveUtilTask_RepeatWithInterval::StaticClass() },
|
||||||
|
{ UDirectiveUtilTask_MoveToLocation::StaticClass() },
|
||||||
|
{ UDirectiveUtilTask_MoveToActor::StaticClass() }
|
||||||
|
};
|
||||||
|
|
||||||
|
TestTrue(TEXT("The runtime module is loaded"), FModuleManager::Get().IsModuleLoaded(TEXT("DirectiveUtilitiesRuntime")));
|
||||||
|
|
||||||
|
#if !WITH_EDITOR
|
||||||
|
TestFalse(TEXT("The editor module is absent from a game target"), FModuleManager::Get().IsModuleLoaded(TEXT("DirectiveUtilitiesEditor")));
|
||||||
|
TestFalse(TEXT("The uncooked node module is absent from a game target"), FModuleManager::Get().IsModuleLoaded(TEXT("DirectiveUtilitiesBlueprintNodes")));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
for (const FClassExpectation& Expectation : Expectations)
|
||||||
|
{
|
||||||
|
TestNotNull(TEXT("Runtime class is reflected"), Expectation.Class);
|
||||||
|
if (!Expectation.Class)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestEqual(
|
||||||
|
FString::Printf(TEXT("%s belongs to the runtime script package"), *Expectation.Class->GetName()),
|
||||||
|
Expectation.Class->GetOutermost()->GetName(),
|
||||||
|
FString(TEXT("/Script/DirectiveUtilitiesRuntime")));
|
||||||
|
|
||||||
|
TestFalse(
|
||||||
|
FString::Printf(TEXT("%s is not in an editor-only package"), *Expectation.Class->GetName()),
|
||||||
|
Expectation.Class->GetOutermost()->HasAnyPackageFlags(PKG_EditorOnly | PKG_UncookedOnly | PKG_Developer));
|
||||||
|
|
||||||
|
for (TFieldIterator<UFunction> FunctionIterator(Expectation.Class, EFieldIteratorFlags::ExcludeSuper); FunctionIterator; ++FunctionIterator)
|
||||||
|
{
|
||||||
|
const UFunction* Function = *FunctionIterator;
|
||||||
|
if (!Function->HasAnyFunctionFlags(FUNC_BlueprintCallable | FUNC_BlueprintPure))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestFalse(
|
||||||
|
FString::Printf(TEXT("%s.%s is available outside the editor"), *Expectation.Class->GetName(), *Function->GetName()),
|
||||||
|
Function->HasAnyFunctionFlags(FUNC_EditorOnly));
|
||||||
|
|
||||||
|
#if WITH_EDITOR
|
||||||
|
const FString Category = Function->GetMetaData(TEXT("Category"));
|
||||||
|
TestTrue(
|
||||||
|
FString::Printf(TEXT("%s.%s has a Directive Utilities category"), *Expectation.Class->GetName(), *Function->GetName()),
|
||||||
|
Category == TEXT("Directive Utilities") || Category.StartsWith(TEXT("Directive Utilities|")));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return !HasAnyErrors();
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
#include "Libraries/DirectiveUtilSaveGameFunctionLibrary.h"
|
#include "Libraries/DirectiveUtilSaveGameFunctionLibrary.h"
|
||||||
#include "Tests/DirectiveUtilTestObject.h"
|
#include "Tests/DirectiveUtilTestObject.h"
|
||||||
#include "Kismet/GameplayStatics.h"
|
#include "Kismet/GameplayStatics.h"
|
||||||
#include "GameFramework/SaveGame.h"
|
#include "GameFramework/SaveGame.h"
|
||||||
#include "Misc/AutomationTest.h"
|
#include "Misc/AutomationTest.h"
|
||||||
|
|
||||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilSaveGameFunctionLibraryTest, "DirectiveUtilities.SaveGameFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilSaveGameFunctionLibraryTest, "DirectiveUtilities.SaveGameFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
bool FDirectiveUtilSaveGameFunctionLibraryTest::RunTest(const FString& Parameters)
|
bool FDirectiveUtilSaveGameFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||||
{
|
{
|
||||||
@@ -84,6 +86,11 @@ bool FDirectiveUtilSaveGameFunctionLibraryTest::RunTest(const FString& Parameter
|
|||||||
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(SlotB));
|
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(SlotB));
|
||||||
TestNotNull("A renamed slot should still deserialize",
|
TestNotNull("A renamed slot should still deserialize",
|
||||||
UGameplayStatics::LoadGameFromSlot(SlotB, 0));
|
UGameplayStatics::LoadGameFromSlot(SlotB, 0));
|
||||||
|
const FString SlotBCaseVariant = SlotB.ToLower();
|
||||||
|
TestTrue("RenameSaveSlot should accept a case-only spelling change",
|
||||||
|
UDirectiveUtilSaveGameFunctionLibrary::RenameSaveSlot(SlotB, SlotBCaseVariant));
|
||||||
|
TestNotNull("A case-only renamed slot should still deserialize",
|
||||||
|
UGameplayStatics::LoadGameFromSlot(SlotBCaseVariant, 0));
|
||||||
|
|
||||||
USaveGame* OtherSave = NewObject<UDirectiveUtilTestSaveGame>();
|
USaveGame* OtherSave = NewObject<UDirectiveUtilTestSaveGame>();
|
||||||
TestTrue("SaveGameToSlot should write the collision test slot",
|
TestTrue("SaveGameToSlot should write the collision test slot",
|
||||||
@@ -99,12 +106,18 @@ bool FDirectiveUtilSaveGameFunctionLibraryTest::RunTest(const FString& Parameter
|
|||||||
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(TEXT("../escape")));
|
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(TEXT("../escape")));
|
||||||
TestFalse("DeleteSaveSlot should reject an invalid slot name",
|
TestFalse("DeleteSaveSlot should reject an invalid slot name",
|
||||||
UDirectiveUtilSaveGameFunctionLibrary::DeleteSaveSlot(TEXT("../escape")));
|
UDirectiveUtilSaveGameFunctionLibrary::DeleteSaveSlot(TEXT("../escape")));
|
||||||
|
TestFalse("DoesSaveSlotExist should reject a nested slot path",
|
||||||
|
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(TEXT("Profiles/Slot1")));
|
||||||
TestFalse("RenameSaveSlot should reject an invalid source slot name",
|
TestFalse("RenameSaveSlot should reject an invalid source slot name",
|
||||||
UDirectiveUtilSaveGameFunctionLibrary::RenameSaveSlot(TEXT("../escape"), SlotC));
|
UDirectiveUtilSaveGameFunctionLibrary::RenameSaveSlot(TEXT("../escape"), SlotC));
|
||||||
TestFalse("RenameSaveSlot should reject an invalid destination slot name",
|
TestFalse("RenameSaveSlot should reject an invalid destination slot name",
|
||||||
UDirectiveUtilSaveGameFunctionLibrary::RenameSaveSlot(SlotC, TEXT("../escape")));
|
UDirectiveUtilSaveGameFunctionLibrary::RenameSaveSlot(SlotC, TEXT("../escape")));
|
||||||
TestTrue("A rejected rename should leave the source slot intact",
|
TestTrue("A rejected rename should leave the source slot intact",
|
||||||
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(SlotC));
|
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(SlotC));
|
||||||
|
TestFalse("DoesSaveSlotExist should reject the reserved device name CON",
|
||||||
|
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(TEXT("CON")));
|
||||||
|
TestFalse("DoesSaveSlotExist should reject CON with an extension",
|
||||||
|
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(TEXT("CON.sav")));
|
||||||
|
|
||||||
TestTrue("DeleteSaveSlot should delete an existing slot",
|
TestTrue("DeleteSaveSlot should delete an existing slot",
|
||||||
UDirectiveUtilSaveGameFunctionLibrary::DeleteSaveSlot(SlotB));
|
UDirectiveUtilSaveGameFunctionLibrary::DeleteSaveSlot(SlotB));
|
||||||
@@ -115,6 +128,7 @@ bool FDirectiveUtilSaveGameFunctionLibraryTest::RunTest(const FString& Parameter
|
|||||||
// Clean up
|
// Clean up
|
||||||
UGameplayStatics::DeleteGameInSlot(SlotA, 0);
|
UGameplayStatics::DeleteGameInSlot(SlotA, 0);
|
||||||
UGameplayStatics::DeleteGameInSlot(SlotB, 0);
|
UGameplayStatics::DeleteGameInSlot(SlotB, 0);
|
||||||
|
UGameplayStatics::DeleteGameInSlot(SlotB.ToLower(), 0);
|
||||||
UGameplayStatics::DeleteGameInSlot(SlotC, 0);
|
UGameplayStatics::DeleteGameInSlot(SlotC, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,37 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
#include "Libraries/DirectiveUtilStringFunctionLibrary.h"
|
#include "Libraries/DirectiveUtilStringFunctionLibrary.h"
|
||||||
#include "Misc/AutomationTest.h"
|
#include "Misc/AutomationTest.h"
|
||||||
|
|
||||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilStringFunctionLibraryTest, "DirectiveUtilities.StringFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
namespace
|
||||||
|
{
|
||||||
|
int32 ReferenceLevenshteinDistance(const FString& Left, const FString& Right)
|
||||||
|
{
|
||||||
|
TArray<int32> PreviousRow;
|
||||||
|
TArray<int32> CurrentRow;
|
||||||
|
PreviousRow.SetNumUninitialized(Right.Len() + 1);
|
||||||
|
CurrentRow.SetNumUninitialized(Right.Len() + 1);
|
||||||
|
for (int32 ColumnIndex = 0; ColumnIndex <= Right.Len(); ++ColumnIndex)
|
||||||
|
{
|
||||||
|
PreviousRow[ColumnIndex] = ColumnIndex;
|
||||||
|
}
|
||||||
|
for (int32 RowIndex = 1; RowIndex <= Left.Len(); ++RowIndex)
|
||||||
|
{
|
||||||
|
CurrentRow[0] = RowIndex;
|
||||||
|
for (int32 ColumnIndex = 1; ColumnIndex <= Right.Len(); ++ColumnIndex)
|
||||||
|
{
|
||||||
|
CurrentRow[ColumnIndex] = FMath::Min3(
|
||||||
|
PreviousRow[ColumnIndex] + 1,
|
||||||
|
CurrentRow[ColumnIndex - 1] + 1,
|
||||||
|
PreviousRow[ColumnIndex - 1] + (Left[RowIndex - 1] == Right[ColumnIndex - 1] ? 0 : 1));
|
||||||
|
}
|
||||||
|
Swap(PreviousRow, CurrentRow);
|
||||||
|
}
|
||||||
|
return PreviousRow.Last();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilStringFunctionLibraryTest, "DirectiveUtilities.StringFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
bool FDirectiveUtilStringFunctionLibraryTest::RunTest(const FString& Parameters)
|
bool FDirectiveUtilStringFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||||
{
|
{
|
||||||
@@ -232,11 +262,26 @@ bool FDirectiveUtilStringFunctionLibraryTest::RunTest(const FString& Parameters)
|
|||||||
TestFalse("IsValidFileName should reject a backslash", 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 an empty string", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("")));
|
||||||
TestFalse("IsValidFileName should reject a reserved character", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("a:b")));
|
TestFalse("IsValidFileName should reject a reserved character", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("a:b")));
|
||||||
|
TestFalse("IsValidFileName should reject the current-directory segment", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT(".")));
|
||||||
|
TestFalse("IsValidFileName should reject the parent-directory segment", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("..")));
|
||||||
|
TestFalse("IsValidFileName should reject the reserved device name CON", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("CON")));
|
||||||
|
TestFalse("IsValidFileName should reject CON with an extension", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("CON.sav")));
|
||||||
|
TestFalse("IsValidFileName should reject NUL case-insensitively", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("nul")));
|
||||||
|
TestFalse("IsValidFileName should reject COM1", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("COM1")));
|
||||||
|
TestFalse("IsValidFileName should reject LPT9", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("LPT9")));
|
||||||
|
TestFalse("IsValidFileName should reject a trailing dot", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("trailing.")));
|
||||||
|
TestFalse("IsValidFileName should reject a run of dots", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("...")));
|
||||||
|
TestTrue("IsValidFileName should accept console, which is not a reserved device name",
|
||||||
|
UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("console")));
|
||||||
|
|
||||||
{
|
{
|
||||||
const FString Sanitized = UDirectiveUtilStringFunctionLibrary::SanitizeFileName(TEXT("../a/b?.sav"));
|
const FString Sanitized = UDirectiveUtilStringFunctionLibrary::SanitizeFileName(TEXT("../a/b?.sav"));
|
||||||
TestTrue("SanitizeFileName should produce a name that IsValidFileName accepts",
|
TestTrue("SanitizeFileName should produce a name that IsValidFileName accepts",
|
||||||
UDirectiveUtilStringFunctionLibrary::IsValidFileName(Sanitized));
|
UDirectiveUtilStringFunctionLibrary::IsValidFileName(Sanitized));
|
||||||
|
const FString SanitizedReserved = UDirectiveUtilStringFunctionLibrary::SanitizeFileName(TEXT("CON"));
|
||||||
|
TestTrue("SanitizeFileName should rewrite a reserved device name into a valid file name",
|
||||||
|
UDirectiveUtilStringFunctionLibrary::IsValidFileName(SanitizedReserved)
|
||||||
|
&& SanitizedReserved == TEXT("_CON"));
|
||||||
const FString Replaced = UDirectiveUtilStringFunctionLibrary::SanitizeFileName(TEXT("a/b"), TEXT("_"));
|
const FString Replaced = UDirectiveUtilStringFunctionLibrary::SanitizeFileName(TEXT("a/b"), TEXT("_"));
|
||||||
TestTrue("SanitizeFileName should substitute the replacement character for stripped characters",
|
TestTrue("SanitizeFileName should substitute the replacement character for stripped characters",
|
||||||
Replaced.Contains(TEXT("_")));
|
Replaced.Contains(TEXT("_")));
|
||||||
@@ -271,6 +316,43 @@ bool FDirectiveUtilStringFunctionLibraryTest::RunTest(const FString& Parameters)
|
|||||||
UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(TEXT(""), {TEXT("abc")}, Similarity), 0);
|
UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(TEXT(""), {TEXT("abc")}, Similarity), 0);
|
||||||
TestTrue("FindBestStringMatch should report similarity 0 for an empty input against a non-empty candidate",
|
TestTrue("FindBestStringMatch should report similarity 0 for an empty input against a non-empty candidate",
|
||||||
FMath::IsNearlyEqual(Similarity, 0.0f, 1.e-4f));
|
FMath::IsNearlyEqual(Similarity, 0.0f, 1.e-4f));
|
||||||
|
|
||||||
|
TestEqual("FindBestStringMatch should keep the first equally close candidate",
|
||||||
|
UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(TEXT("cat"), {TEXT("bat"), TEXT("hat")}, Similarity), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TestEqual("Levenshtein should trim equal prefixes and suffixes without changing the result",
|
||||||
|
UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(TEXT("shared-prefix-A-shared-suffix"), TEXT("shared-prefix-B-shared-suffix")), 1);
|
||||||
|
TestEqual("Levenshtein should support Unicode code units",
|
||||||
|
UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(TEXT("café-one"), TEXT("café-two")), 3);
|
||||||
|
|
||||||
|
FRandomStream LevenshteinStream(90210);
|
||||||
|
for (int32 Iteration = 0; Iteration < 200; ++Iteration)
|
||||||
|
{
|
||||||
|
FString Left;
|
||||||
|
FString Right;
|
||||||
|
const int32 LeftLength = LevenshteinStream.RandRange(0, 64);
|
||||||
|
const int32 RightLength = LevenshteinStream.RandRange(0, 64);
|
||||||
|
Left.Reserve(LeftLength);
|
||||||
|
Right.Reserve(RightLength);
|
||||||
|
for (int32 Index = 0; Index < LeftLength; ++Index)
|
||||||
|
{
|
||||||
|
Left.AppendChar(static_cast<TCHAR>(TEXT('a') + LevenshteinStream.RandRange(0, 5)));
|
||||||
|
}
|
||||||
|
for (int32 Index = 0; Index < RightLength; ++Index)
|
||||||
|
{
|
||||||
|
Right.AppendChar(static_cast<TCHAR>(TEXT('a') + LevenshteinStream.RandRange(0, 5)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const int32 ExpectedDistance = ReferenceLevenshteinDistance(Left, Right);
|
||||||
|
TestEqual(
|
||||||
|
FString::Printf(TEXT("Levenshtein fuzz case %d"), Iteration),
|
||||||
|
UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(Left, Right),
|
||||||
|
ExpectedDistance);
|
||||||
|
TestEqual(
|
||||||
|
FString::Printf(TEXT("Levenshtein symmetry case %d"), Iteration),
|
||||||
|
UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(Right, Left),
|
||||||
|
ExpectedDistance);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
|
#include "Libraries/DirectiveUtilStringFunctionLibrary.h"
|
||||||
|
|
||||||
|
#include "Misc/AutomationTest.h"
|
||||||
|
|
||||||
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(
|
||||||
|
FDirectiveUtilStringCardinalityTest,
|
||||||
|
"DirectiveUtilities.StringScenarios.Cardinality",
|
||||||
|
EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
|
bool FDirectiveUtilStringCardinalityTest::RunTest(const FString& Parameters)
|
||||||
|
{
|
||||||
|
for (const int32 CharacterCount : {0, 1, 2, 31, 32, 255, 256, 1024})
|
||||||
|
{
|
||||||
|
FString Source;
|
||||||
|
Source.Reserve(CharacterCount);
|
||||||
|
for (int32 Index = 0; Index < CharacterCount; ++Index)
|
||||||
|
{
|
||||||
|
Source.AppendChar(TEXT('a'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const FString Label = FString::Printf(TEXT("characters=%d"), CharacterCount);
|
||||||
|
TestEqual(
|
||||||
|
Label + TEXT(" identical"),
|
||||||
|
UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(Source, Source),
|
||||||
|
0);
|
||||||
|
TestEqual(
|
||||||
|
Label + TEXT(" empty comparison"),
|
||||||
|
UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(Source, FString()),
|
||||||
|
CharacterCount);
|
||||||
|
|
||||||
|
if (!Source.IsEmpty())
|
||||||
|
{
|
||||||
|
for (const int32 ChangedIndex : {0, CharacterCount / 2, CharacterCount - 1})
|
||||||
|
{
|
||||||
|
FString Changed = Source;
|
||||||
|
Changed[ChangedIndex] = TEXT('b');
|
||||||
|
TestEqual(
|
||||||
|
FString::Printf(TEXT("characters=%d changed=%d"), CharacterCount, ChangedIndex),
|
||||||
|
UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(Source, Changed),
|
||||||
|
1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const int32 CandidateCount : {0, 1, 2, 31, 32, 256, 1000})
|
||||||
|
{
|
||||||
|
TArray<FString> Candidates;
|
||||||
|
Candidates.Reserve(CandidateCount);
|
||||||
|
for (int32 Index = 0; Index < CandidateCount; ++Index)
|
||||||
|
{
|
||||||
|
Candidates.Add(FString::Printf(TEXT("Candidate%04d"), Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
float Similarity = -1.0f;
|
||||||
|
if (Candidates.IsEmpty())
|
||||||
|
{
|
||||||
|
TestEqual(
|
||||||
|
"empty candidate set index",
|
||||||
|
UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(TEXT("Candidate0000"), Candidates, Similarity),
|
||||||
|
INDEX_NONE);
|
||||||
|
TestEqual("empty candidate set similarity", Similarity, 0.0f);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TArray<int32> ExpectedIndices = {0, CandidateCount / 2, CandidateCount - 1};
|
||||||
|
for (const int32 ExpectedIndex : ExpectedIndices)
|
||||||
|
{
|
||||||
|
Similarity = -1.0f;
|
||||||
|
const int32 MatchIndex = UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(
|
||||||
|
Candidates[ExpectedIndex],
|
||||||
|
Candidates,
|
||||||
|
Similarity);
|
||||||
|
const FString Label = FString::Printf(TEXT("candidates=%d expected=%d"), CandidateCount, ExpectedIndex);
|
||||||
|
TestEqual(Label + TEXT(" index"), MatchIndex, ExpectedIndex);
|
||||||
|
TestEqual(Label + TEXT(" similarity"), Similarity, 1.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||||
|
|
||||||
#include "Libraries/DirectiveUtilTextFunctionLibrary.h"
|
#include "Libraries/DirectiveUtilTextFunctionLibrary.h"
|
||||||
#include "Misc/AutomationTest.h"
|
#include "Misc/AutomationTest.h"
|
||||||
|
|
||||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilTextFunctionLibraryTest, "DirectiveUtilities.TextFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilTextFunctionLibraryTest, "DirectiveUtilities.TextFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||||
|
|
||||||
bool FDirectiveUtilTextFunctionLibraryTest::RunTest(const FString& Parameters)
|
bool FDirectiveUtilTextFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||||
{
|
{
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user