Update directive utilities plugin
This commit is contained in:
Binary file not shown.
@@ -6,8 +6,5 @@
|
||||
; /README.txt
|
||||
; /Extras/...
|
||||
; /Binaries/ThirdParty/*.dll
|
||||
/README.md
|
||||
/LICENSE
|
||||
/CHANGELOG.md
|
||||
/Documentation/...
|
||||
/Config/DefaultDirectiveUtilities.ini
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
{
|
||||
"FileVersion": 3,
|
||||
"Version": 3,
|
||||
"VersionName": "2.0",
|
||||
"Version": 7,
|
||||
"VersionName": "2.2.0",
|
||||
"FriendlyName": "Directive Utilities",
|
||||
"Description": "An open-source Unreal Engine plugin that provides runtime and editor utility nodes for developers.",
|
||||
"Category": "Unreal Directive",
|
||||
"CreatedBy": "Unreal Directive",
|
||||
"CreatedByURL": "https://unrealdirective.com",
|
||||
"DocsURL": "https://udcore.unrealdirective.com/",
|
||||
"MarketplaceURL": "https://unrealdirective.com",
|
||||
"DocsURL": "https://github.com/UnrealDirective/DirectiveUtilities/tree/main/Documentation",
|
||||
"MarketplaceURL": "",
|
||||
"SupportURL": "https://github.com/UnrealDirective/DirectiveUtilities/issues",
|
||||
"EngineVersion": "5.8.0",
|
||||
"CanContainContent": false,
|
||||
"IsBetaVersion": false,
|
||||
"IsExperimentalVersion": false,
|
||||
"Installed": false,
|
||||
"Installed": true,
|
||||
"SupportedTargetPlatforms": [
|
||||
"Win64",
|
||||
"Mac",
|
||||
"Linux"
|
||||
],
|
||||
"Modules": [
|
||||
{
|
||||
"Name": "DirectiveUtilitiesRuntime",
|
||||
@@ -25,10 +31,25 @@
|
||||
"Linux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "DirectiveUtilitiesBlueprintNodes",
|
||||
"Type": "UncookedOnly",
|
||||
"LoadingPhase": "Default",
|
||||
"PlatformAllowList": [
|
||||
"Win64",
|
||||
"Mac",
|
||||
"Linux"
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "DirectiveUtilitiesEditor",
|
||||
"Type": "Editor",
|
||||
"LoadingPhase": "Default",
|
||||
"PlatformAllowList": [
|
||||
"Win64",
|
||||
"Mac",
|
||||
"Linux"
|
||||
],
|
||||
"TargetAllowList": [
|
||||
"Editor"
|
||||
]
|
||||
@@ -50,7 +71,10 @@
|
||||
"Plugins": [
|
||||
{
|
||||
"Name": "EditorScriptingUtilities",
|
||||
"Enabled": true
|
||||
"Enabled": true,
|
||||
"TargetAllowList": [
|
||||
"Editor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"EditorSubsystem",
|
||||
"EditorScriptingUtilities",
|
||||
"AssetRegistry",
|
||||
"DirectiveUtilitiesRuntime",
|
||||
}
|
||||
);
|
||||
@@ -26,7 +27,6 @@ public class DirectiveUtilitiesEditor : ModuleRules
|
||||
"Slate",
|
||||
"SlateCore",
|
||||
"UnrealEd",
|
||||
"AssetRegistry",
|
||||
"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.
|
||||
|
||||
#include "DirectiveUtilitiesEditor.h"
|
||||
#include "Tasks/DirectiveUtilEditorSlowTask.h"
|
||||
|
||||
void FDirectiveUtilitiesEditorModule::StartupModule()
|
||||
{
|
||||
@@ -8,6 +9,7 @@ void FDirectiveUtilitiesEditorModule::StartupModule()
|
||||
|
||||
void FDirectiveUtilitiesEditorModule::ShutdownModule()
|
||||
{
|
||||
UDirectiveUtilEditorSlowTask::FinishActiveTask();
|
||||
}
|
||||
|
||||
IMPLEMENT_MODULE(FDirectiveUtilitiesEditorModule, DirectiveUtilitiesEditor)
|
||||
|
||||
@@ -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 "AssetRegistry/DirectiveUtilAssetRegistry.h"
|
||||
#include "Editor.h"
|
||||
#include "Subsystems/EditorAssetSubsystem.h"
|
||||
#include "Algo/Transform.h"
|
||||
#include "AssetRegistry/IAssetRegistry.h"
|
||||
#include "AssetRegistry/ARFilter.h"
|
||||
#include "AssetToolsModule.h"
|
||||
#include "Editor.h"
|
||||
#include "IAssetTools.h"
|
||||
#include "Misc/App.h"
|
||||
#include "Misc/AssetRegistryInterface.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
#include "Runtime/Launch/Resources/Version.h"
|
||||
#include "UObject/ObjectRedirector.h"
|
||||
|
||||
namespace
|
||||
@@ -18,10 +23,7 @@ namespace
|
||||
{
|
||||
if (IAssetRegistry* AssetRegistry = IAssetRegistry::Get())
|
||||
{
|
||||
if (AssetRegistry->IsLoadingAssets())
|
||||
{
|
||||
AssetRegistry->WaitForCompletion();
|
||||
}
|
||||
DirectiveUtilitiesEditor::EnsureAssetRegistryScan(*AssetRegistry);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,10 +118,7 @@ EDirectiveUtilSuccessStatus UDirectiveUtilEditorAssetLibrary::FixUpRedirectorsIn
|
||||
return EDirectiveUtilSuccessStatus::Failure;
|
||||
}
|
||||
|
||||
if (AssetRegistry->IsLoadingAssets())
|
||||
{
|
||||
AssetRegistry->WaitForCompletion();
|
||||
}
|
||||
DirectiveUtilitiesEditor::EnsureAssetRegistryScan(*AssetRegistry);
|
||||
|
||||
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
|
||||
IAssetTools& AssetTools = AssetToolsModule.Get();
|
||||
@@ -160,6 +159,11 @@ EDirectiveUtilSuccessStatus UDirectiveUtilEditorAssetLibrary::FixUpRedirectorsIn
|
||||
{
|
||||
return EDirectiveUtilSuccessStatus::Success;
|
||||
}
|
||||
if (FApp::IsUnattended())
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("Redirector fix-up requires an interactive editor session."));
|
||||
return EDirectiveUtilSuccessStatus::Failure;
|
||||
}
|
||||
|
||||
AssetTools.FixupReferencers(Redirectors, false, ERedirectFixupMode::DeleteFixedUpRedirectors);
|
||||
OutRedirectorsProcessed = Redirectors.Num();
|
||||
|
||||
@@ -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 "DirectiveUtilLogChannels.h"
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
#include "Editor.h"
|
||||
#include "Engine/StaticMeshActor.h"
|
||||
#include "Engine/StaticMesh.h"
|
||||
#include "Engine/Texture.h"
|
||||
#include "Engine/Texture2D.h"
|
||||
#include "Materials/Material.h"
|
||||
#include "Materials/MaterialInterface.h"
|
||||
#include "Materials/MaterialExpressionTextureObject.h"
|
||||
#include "EditorViewportClient.h"
|
||||
#include "LevelEditorViewport.h"
|
||||
#include "Components/BoxComponent.h"
|
||||
#include "Components/CapsuleComponent.h"
|
||||
#include "Components/SphereComponent.h"
|
||||
#include "Materials/MaterialExpressionTextureSample.h"
|
||||
#include "Runtime/Launch/Resources/Version.h"
|
||||
#include "ScopedTransaction.h"
|
||||
#include "Engine/World.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
bool IsFiniteVector(const FVector& Value)
|
||||
{
|
||||
return FMath::IsFinite(Value.X) && FMath::IsFinite(Value.Y) && FMath::IsFinite(Value.Z);
|
||||
}
|
||||
|
||||
bool IsFiniteQuat(const FQuat& Value)
|
||||
{
|
||||
return FMath::IsFinite(Value.X)
|
||||
&& FMath::IsFinite(Value.Y)
|
||||
&& FMath::IsFinite(Value.Z)
|
||||
&& FMath::IsFinite(Value.W);
|
||||
}
|
||||
|
||||
bool MatchesTextureReference(const UTexture* Texture, const TSoftObjectPtr<UTexture2D>& TextureReference)
|
||||
{
|
||||
return TextureReference.IsNull()
|
||||
? Texture == nullptr
|
||||
: Texture && FSoftObjectPath(Texture) == TextureReference.ToSoftObjectPath();
|
||||
}
|
||||
|
||||
// Adds an actor to OutActors when whether it matches the predicate (evaluated once across all of the
|
||||
// actor's components) equals whether matches are being included. Centralizes the include/exclude
|
||||
// aggregation so a multi-component actor is judged per-actor rather than per-component.
|
||||
@@ -39,7 +69,7 @@ namespace
|
||||
|
||||
for (TSourceActor* Actor : *Source)
|
||||
{
|
||||
if (!Actor || Seen.Contains(Actor)) { continue; }
|
||||
if (!IsValid(Actor) || Seen.Contains(Actor)) { continue; }
|
||||
if (ActorMatches(Actor) == bIncludeMatches)
|
||||
{
|
||||
Seen.Add(Actor);
|
||||
@@ -47,27 +77,163 @@ namespace
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FActorLayoutData
|
||||
{
|
||||
AActor* Actor = nullptr;
|
||||
FBox Bounds = FBox(ForceInit);
|
||||
FVector Location = FVector::ZeroVector;
|
||||
int32 AttachmentDepth = 0;
|
||||
};
|
||||
|
||||
float GetAxisValue(const FVector& Vector, const EDirectiveUtilActorLayoutAxis Axis)
|
||||
{
|
||||
switch (Axis)
|
||||
{
|
||||
case EDirectiveUtilActorLayoutAxis::X:
|
||||
return Vector.X;
|
||||
case EDirectiveUtilActorLayoutAxis::Y:
|
||||
return Vector.Y;
|
||||
case EDirectiveUtilActorLayoutAxis::Z:
|
||||
return Vector.Z;
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
void SetAxisValue(FVector& Vector, const EDirectiveUtilActorLayoutAxis Axis, const float Value)
|
||||
{
|
||||
switch (Axis)
|
||||
{
|
||||
case EDirectiveUtilActorLayoutAxis::X:
|
||||
Vector.X = Value;
|
||||
break;
|
||||
case EDirectiveUtilActorLayoutAxis::Y:
|
||||
Vector.Y = Value;
|
||||
break;
|
||||
case EDirectiveUtilActorLayoutAxis::Z:
|
||||
Vector.Z = Value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int32 GetAttachmentDepth(const AActor* Actor)
|
||||
{
|
||||
int32 Depth = 0;
|
||||
TSet<const AActor*> Visited;
|
||||
for (const AActor* Parent = Actor ? Actor->GetAttachParentActor() : nullptr;
|
||||
Parent && !Visited.Contains(Parent);
|
||||
Parent = Parent->GetAttachParentActor())
|
||||
{
|
||||
Visited.Add(Parent);
|
||||
++Depth;
|
||||
}
|
||||
return Depth;
|
||||
}
|
||||
|
||||
bool IsLayoutActorValid(AActor* Actor)
|
||||
{
|
||||
return IsValid(Actor)
|
||||
&& Actor->GetWorld()
|
||||
&& !Actor->HasAnyFlags(RF_Transient)
|
||||
&& !Actor->IsActorBeingDestroyed();
|
||||
}
|
||||
|
||||
TArray<FActorLayoutData> GetLayoutActors(
|
||||
const TArray<AActor*>& Actors,
|
||||
FDirectiveUtilActorOperationResult& Result)
|
||||
{
|
||||
TArray<FActorLayoutData> LayoutActors;
|
||||
TSet<AActor*> Seen;
|
||||
for (AActor* Actor : Actors)
|
||||
{
|
||||
if (!Actor || Seen.Contains(Actor))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Seen.Add(Actor);
|
||||
|
||||
if (!IsLayoutActorValid(Actor))
|
||||
{
|
||||
Result.SkippedActors.Add(Actor);
|
||||
continue;
|
||||
}
|
||||
|
||||
FActorLayoutData Data;
|
||||
Data.Actor = Actor;
|
||||
Data.Location = Actor->GetActorLocation();
|
||||
if (!IsFiniteVector(Data.Location)
|
||||
|| !IsFiniteVector(Actor->GetActorScale3D())
|
||||
|| !IsFiniteQuat(Actor->GetActorQuat()))
|
||||
{
|
||||
Result.SkippedActors.Add(Actor);
|
||||
continue;
|
||||
}
|
||||
|
||||
Data.Bounds = Actor->GetComponentsBoundingBox(true, true);
|
||||
if (!Data.Bounds.IsValid)
|
||||
{
|
||||
Data.Bounds = FBox(Data.Location, Data.Location);
|
||||
}
|
||||
if (!IsFiniteVector(Data.Bounds.Min) || !IsFiniteVector(Data.Bounds.Max))
|
||||
{
|
||||
Result.SkippedActors.Add(Actor);
|
||||
continue;
|
||||
}
|
||||
Data.AttachmentDepth = GetAttachmentDepth(Actor);
|
||||
LayoutActors.Add(MoveTemp(Data));
|
||||
}
|
||||
return LayoutActors;
|
||||
}
|
||||
|
||||
void ApplyLocations(
|
||||
TArray<FActorLayoutData>& LayoutActors,
|
||||
const TMap<AActor*, FVector>& Locations,
|
||||
FDirectiveUtilActorOperationResult& Result)
|
||||
{
|
||||
LayoutActors.Sort([](const FActorLayoutData& Left, const FActorLayoutData& Right) {
|
||||
return Left.AttachmentDepth < Right.AttachmentDepth;
|
||||
});
|
||||
|
||||
for (const FActorLayoutData& Data : LayoutActors)
|
||||
{
|
||||
const FVector* Location = Locations.Find(Data.Actor);
|
||||
if (!Location || !IsFiniteVector(*Location) || Data.Actor->GetActorLocation().Equals(*Location))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Data.Actor->Modify();
|
||||
if (Data.Actor->SetActorLocation(*Location, false, nullptr, ETeleportType::TeleportPhysics))
|
||||
{
|
||||
Result.ChangedActors.Add(Data.Actor);
|
||||
}
|
||||
else
|
||||
{
|
||||
Result.SkippedActors.Add(Data.Actor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UDirectiveUtilEditorActorSubsystem::FocusActorsInViewport(const TArray<AActor*> Actors, const bool bInstant)
|
||||
{
|
||||
if (Actors.Num() == 0) { return; }
|
||||
if (!GEditor) { return; }
|
||||
|
||||
FViewport* ActiveViewport = GEditor->GetActiveViewport();
|
||||
if (!ActiveViewport) { return; }
|
||||
|
||||
FEditorViewportClient* ViewportClient = static_cast<FEditorViewportClient*>(ActiveViewport->GetClient());
|
||||
if (!ViewportClient) { return; }
|
||||
if (!GCurrentLevelEditingViewportClient) { return; }
|
||||
|
||||
FBox BoundingBox = FBox(ForceInit);
|
||||
for (const auto Actor : Actors)
|
||||
for (const AActor* Actor : Actors)
|
||||
{
|
||||
if (!Actor) { continue; }
|
||||
BoundingBox += Actor->GetComponentsBoundingBox(true, true);
|
||||
if (!IsValid(Actor)) { continue; }
|
||||
const FBox ActorBounds = Actor->GetComponentsBoundingBox(true, true);
|
||||
if (ActorBounds.IsValid)
|
||||
{
|
||||
BoundingBox += ActorBounds;
|
||||
}
|
||||
}
|
||||
|
||||
ViewportClient->FocusViewportOnBox(BoundingBox, bInstant);
|
||||
if (BoundingBox.IsValid)
|
||||
{
|
||||
GCurrentLevelEditingViewportClient->FocusViewportOnBox(BoundingBox, bInstant);
|
||||
}
|
||||
}
|
||||
|
||||
TArray<UClass*> UDirectiveUtilEditorActorSubsystem::GetAllLevelClasses()
|
||||
@@ -84,6 +250,236 @@ TArray<UClass*> UDirectiveUtilEditorActorSubsystem::GetAllLevelClasses()
|
||||
return ActorClasses;
|
||||
}
|
||||
|
||||
FDirectiveUtilActorOperationResult UDirectiveUtilEditorActorSubsystem::AlignActors(
|
||||
const TArray<AActor*>& Actors,
|
||||
const EDirectiveUtilActorLayoutAxis Axis,
|
||||
const EDirectiveUtilActorAlignment Alignment)
|
||||
{
|
||||
FDirectiveUtilActorOperationResult Result;
|
||||
TArray<FActorLayoutData> LayoutActors = GetLayoutActors(Actors, Result);
|
||||
if (LayoutActors.Num() < 2)
|
||||
{
|
||||
return Result;
|
||||
}
|
||||
|
||||
FBox CombinedBounds(ForceInit);
|
||||
for (const FActorLayoutData& Data : LayoutActors)
|
||||
{
|
||||
CombinedBounds += Data.Bounds;
|
||||
}
|
||||
|
||||
float Target = 0.0f;
|
||||
switch (Alignment)
|
||||
{
|
||||
case EDirectiveUtilActorAlignment::Minimum:
|
||||
Target = GetAxisValue(CombinedBounds.Min, Axis);
|
||||
break;
|
||||
case EDirectiveUtilActorAlignment::Center:
|
||||
Target = GetAxisValue(CombinedBounds.GetCenter(), Axis);
|
||||
break;
|
||||
case EDirectiveUtilActorAlignment::Maximum:
|
||||
Target = GetAxisValue(CombinedBounds.Max, Axis);
|
||||
break;
|
||||
}
|
||||
|
||||
TMap<AActor*, FVector> Locations;
|
||||
for (const FActorLayoutData& Data : LayoutActors)
|
||||
{
|
||||
const float Current = Alignment == EDirectiveUtilActorAlignment::Minimum
|
||||
? GetAxisValue(Data.Bounds.Min, Axis)
|
||||
: Alignment == EDirectiveUtilActorAlignment::Maximum
|
||||
? GetAxisValue(Data.Bounds.Max, Axis)
|
||||
: GetAxisValue(Data.Bounds.GetCenter(), Axis);
|
||||
FVector Location = Data.Location;
|
||||
SetAxisValue(Location, Axis, GetAxisValue(Location, Axis) + Target - Current);
|
||||
Locations.Add(Data.Actor, Location);
|
||||
}
|
||||
|
||||
const FScopedTransaction Transaction(NSLOCTEXT("DirectiveUtilities", "AlignActors", "Align Actors"));
|
||||
ApplyLocations(LayoutActors, Locations, Result);
|
||||
return Result;
|
||||
}
|
||||
|
||||
FDirectiveUtilActorOperationResult UDirectiveUtilEditorActorSubsystem::DistributeActors(
|
||||
const TArray<AActor*>& Actors,
|
||||
const EDirectiveUtilActorLayoutAxis Axis,
|
||||
const EDirectiveUtilActorDistribution Distribution)
|
||||
{
|
||||
FDirectiveUtilActorOperationResult Result;
|
||||
TArray<FActorLayoutData> LayoutActors = GetLayoutActors(Actors, Result);
|
||||
if (LayoutActors.Num() < 3)
|
||||
{
|
||||
return Result;
|
||||
}
|
||||
|
||||
LayoutActors.Sort([Axis](const FActorLayoutData& Left, const FActorLayoutData& Right) {
|
||||
return GetAxisValue(Left.Bounds.GetCenter(), Axis) < GetAxisValue(Right.Bounds.GetCenter(), Axis);
|
||||
});
|
||||
|
||||
TMap<AActor*, FVector> Locations;
|
||||
if (Distribution == EDirectiveUtilActorDistribution::Centers)
|
||||
{
|
||||
const float Start = GetAxisValue(LayoutActors[0].Bounds.GetCenter(), Axis);
|
||||
const float End = GetAxisValue(LayoutActors.Last().Bounds.GetCenter(), Axis);
|
||||
const float Spacing = (End - Start) / (LayoutActors.Num() - 1);
|
||||
for (int32 Index = 1; Index < LayoutActors.Num() - 1; ++Index)
|
||||
{
|
||||
const FActorLayoutData& Data = LayoutActors[Index];
|
||||
FVector Location = Data.Location;
|
||||
const float Delta = Start + Spacing * Index - GetAxisValue(Data.Bounds.GetCenter(), Axis);
|
||||
SetAxisValue(Location, Axis, GetAxisValue(Location, Axis) + Delta);
|
||||
Locations.Add(Data.Actor, Location);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
float TotalSize = 0.0f;
|
||||
for (const FActorLayoutData& Data : LayoutActors)
|
||||
{
|
||||
TotalSize += GetAxisValue(Data.Bounds.GetSize(), Axis);
|
||||
}
|
||||
const float Start = GetAxisValue(LayoutActors[0].Bounds.Min, Axis);
|
||||
const float End = GetAxisValue(LayoutActors.Last().Bounds.Max, Axis);
|
||||
const float Gap = (End - Start - TotalSize) / (LayoutActors.Num() - 1);
|
||||
float NextMinimum = GetAxisValue(LayoutActors[0].Bounds.Max, Axis) + Gap;
|
||||
for (int32 Index = 1; Index < LayoutActors.Num() - 1; ++Index)
|
||||
{
|
||||
const FActorLayoutData& Data = LayoutActors[Index];
|
||||
FVector Location = Data.Location;
|
||||
const float Delta = NextMinimum - GetAxisValue(Data.Bounds.Min, Axis);
|
||||
SetAxisValue(Location, Axis, GetAxisValue(Location, Axis) + Delta);
|
||||
Locations.Add(Data.Actor, Location);
|
||||
NextMinimum += GetAxisValue(Data.Bounds.GetSize(), Axis) + Gap;
|
||||
}
|
||||
}
|
||||
|
||||
const FScopedTransaction Transaction(NSLOCTEXT("DirectiveUtilities", "DistributeActors", "Distribute Actors"));
|
||||
ApplyLocations(LayoutActors, Locations, Result);
|
||||
return Result;
|
||||
}
|
||||
|
||||
FDirectiveUtilActorOperationResult UDirectiveUtilEditorActorSubsystem::SnapActorsToSurface(
|
||||
const TArray<AActor*>& Actors,
|
||||
const FVector TraceDirection,
|
||||
const float MaximumDistance,
|
||||
const TEnumAsByte<ECollisionChannel> TraceChannel,
|
||||
const EDirectiveUtilSurfacePlacement Placement,
|
||||
const bool bAlignToNormal)
|
||||
{
|
||||
FDirectiveUtilActorOperationResult Result;
|
||||
TArray<FActorLayoutData> LayoutActors = GetLayoutActors(Actors, Result);
|
||||
if (LayoutActors.IsEmpty()
|
||||
|| !FMath::IsFinite(MaximumDistance)
|
||||
|| MaximumDistance <= 0.0f
|
||||
|| !IsFiniteVector(TraceDirection)
|
||||
|| TraceDirection.IsNearlyZero()
|
||||
|| TraceChannel.GetValue() >= ECC_MAX)
|
||||
{
|
||||
for (const FActorLayoutData& Data : LayoutActors)
|
||||
{
|
||||
Result.SkippedActors.Add(Data.Actor);
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
const FVector Direction = TraceDirection.GetSafeNormal();
|
||||
FCollisionQueryParams QueryParams(SCENE_QUERY_STAT(DirectiveUtilitiesSnapActors), true);
|
||||
for (const FActorLayoutData& Data : LayoutActors)
|
||||
{
|
||||
QueryParams.AddIgnoredActor(Data.Actor);
|
||||
}
|
||||
|
||||
struct FSnapTarget
|
||||
{
|
||||
FVector Location;
|
||||
FQuat Rotation;
|
||||
};
|
||||
TMap<AActor*, FSnapTarget> Targets;
|
||||
for (const FActorLayoutData& Data : LayoutActors)
|
||||
{
|
||||
FHitResult Hit;
|
||||
const FVector Start = Data.Location;
|
||||
if (!Data.Actor->GetWorld()->LineTraceSingleByChannel(
|
||||
Hit,
|
||||
Start,
|
||||
Start + Direction * MaximumDistance,
|
||||
TraceChannel,
|
||||
QueryParams))
|
||||
{
|
||||
Result.SkippedActors.Add(Data.Actor);
|
||||
continue;
|
||||
}
|
||||
if (!IsFiniteVector(Hit.Location)
|
||||
|| (bAlignToNormal && (!IsFiniteVector(Hit.ImpactNormal) || Hit.ImpactNormal.IsNearlyZero())))
|
||||
{
|
||||
Result.SkippedActors.Add(Data.Actor);
|
||||
continue;
|
||||
}
|
||||
|
||||
FVector Location = Hit.Location;
|
||||
if (Placement == EDirectiveUtilSurfacePlacement::Bounds)
|
||||
{
|
||||
const FVector Extent = Data.Bounds.GetExtent();
|
||||
const float Support = FMath::Abs(Direction.X) * Extent.X
|
||||
+ FMath::Abs(Direction.Y) * Extent.Y
|
||||
+ FMath::Abs(Direction.Z) * Extent.Z;
|
||||
Location = Hit.Location - Direction * Support - (Data.Bounds.GetCenter() - Data.Location);
|
||||
}
|
||||
|
||||
FQuat Rotation = Data.Actor->GetActorQuat();
|
||||
if (bAlignToNormal)
|
||||
{
|
||||
Rotation = FQuat::FindBetweenNormals(Rotation.GetUpVector(), Hit.ImpactNormal) * Rotation;
|
||||
Rotation.Normalize();
|
||||
}
|
||||
if (IsFiniteVector(Location) && IsFiniteQuat(Rotation))
|
||||
{
|
||||
Targets.Add(Data.Actor, {Location, Rotation});
|
||||
}
|
||||
else
|
||||
{
|
||||
Result.SkippedActors.Add(Data.Actor);
|
||||
}
|
||||
}
|
||||
|
||||
LayoutActors.Sort([](const FActorLayoutData& Left, const FActorLayoutData& Right) {
|
||||
return Left.AttachmentDepth < Right.AttachmentDepth;
|
||||
});
|
||||
const FScopedTransaction Transaction(NSLOCTEXT("DirectiveUtilities", "SnapActorsToSurface", "Snap Actors To Surface"));
|
||||
for (const FActorLayoutData& Data : LayoutActors)
|
||||
{
|
||||
const FSnapTarget* Target = Targets.Find(Data.Actor);
|
||||
if (!Target)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool bLocationChanged = !Data.Actor->GetActorLocation().Equals(Target->Location);
|
||||
const bool bRotationChanged = !Data.Actor->GetActorQuat().Equals(Target->Rotation);
|
||||
if (!bLocationChanged && !bRotationChanged)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Data.Actor->Modify();
|
||||
const bool bMoved = Data.Actor->SetActorLocationAndRotation(
|
||||
Target->Location,
|
||||
Target->Rotation,
|
||||
false,
|
||||
nullptr,
|
||||
ETeleportType::TeleportPhysics);
|
||||
if (bMoved)
|
||||
{
|
||||
Result.ChangedActors.Add(Data.Actor);
|
||||
}
|
||||
else
|
||||
{
|
||||
Result.SkippedActors.Add(Data.Actor);
|
||||
}
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
void UDirectiveUtilEditorActorSubsystem::FilterStaticMeshActors(
|
||||
TArray<AStaticMeshActor*>& OutStaticMeshActors,
|
||||
TArray<AActor*> ActorsToFilter) const
|
||||
@@ -735,11 +1131,11 @@ void UDirectiveUtilEditorActorSubsystem::FilterActorsByTexture(
|
||||
{
|
||||
if (const UMaterialExpressionTextureSample* TextureSample = Cast<UMaterialExpressionTextureSample>(Expression))
|
||||
{
|
||||
if (TextureSample->Texture == TextureReference) { return true; }
|
||||
if (MatchesTextureReference(TextureSample->Texture, TextureReference)) { return true; }
|
||||
}
|
||||
if (const UMaterialExpressionTextureObject* TextureObject = Cast<UMaterialExpressionTextureObject>(Expression))
|
||||
{
|
||||
if (TextureObject->Texture == TextureReference) { return true; }
|
||||
if (MatchesTextureReference(TextureObject->Texture, TextureReference)) { return true; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -757,11 +1153,11 @@ void UDirectiveUtilEditorActorSubsystem::FilterActorsByTexture(
|
||||
{
|
||||
if (const UMaterialExpressionTextureSample* TextureSample = Cast<UMaterialExpressionTextureSample>(Expression))
|
||||
{
|
||||
if (TextureSample->Texture == TextureReference) { return true; }
|
||||
if (MatchesTextureReference(TextureSample->Texture, TextureReference)) { return true; }
|
||||
}
|
||||
if (const UMaterialExpressionTextureObject* TextureObject = Cast<UMaterialExpressionTextureObject>(Expression))
|
||||
{
|
||||
if (TextureObject->Texture == TextureReference) { return true; }
|
||||
if (MatchesTextureReference(TextureObject->Texture, TextureReference)) { return true; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -901,36 +1297,7 @@ void UDirectiveUtilEditorActorSubsystem::GetActorsByMaterialSoftReference(
|
||||
const EDirectiveUtilInclusivity Inclusivity)
|
||||
{
|
||||
const TArray<AActor*> SourceActors = SelectionMethod == Selection ? GetSelectedLevelActors() : GetAllLevelActors();
|
||||
|
||||
TArray<AStaticMeshActor*> StaticMeshActors;
|
||||
FilterStaticMeshActors(StaticMeshActors, SourceActors);
|
||||
|
||||
FilterActorsByPredicate(StaticMeshActors, FoundActors, Inclusivity, [&](AStaticMeshActor* StaticMeshActor) -> bool
|
||||
{
|
||||
const UStaticMeshComponent* StaticMeshComp = StaticMeshActor->GetStaticMeshComponent();
|
||||
if (!StaticMeshComp) { return false; }
|
||||
const UStaticMesh* Mesh = StaticMeshComp->GetStaticMesh();
|
||||
if (!Mesh) { return false; }
|
||||
|
||||
if (MaterialSource == BaseAndOverride || MaterialSource == OverrideOnly)
|
||||
{
|
||||
for (int32 i = 0; i < StaticMeshComp->GetNumMaterials(); i++)
|
||||
{
|
||||
if (StaticMeshComp->GetMaterial(i) == Material) { return true; }
|
||||
}
|
||||
}
|
||||
if (MaterialSource == BaseAndOverride || MaterialSource == BaseOnly)
|
||||
{
|
||||
for (int32 i = 0; i < Mesh->GetStaticMaterials().Num(); i++)
|
||||
{
|
||||
if (Mesh->GetMaterial(i) == Material) { return true; }
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
UE_LOG(LogDirectiveUtilEditor, Display, TEXT("%i actors with the material reference were found."),
|
||||
FoundActors.Num());
|
||||
FilterActorsByMaterial(SourceActors, FoundActors, Material, MaterialSource, Inclusivity);
|
||||
}
|
||||
|
||||
void UDirectiveUtilEditorActorSubsystem::GetActorsByMaterialName(
|
||||
@@ -941,38 +1308,7 @@ void UDirectiveUtilEditorActorSubsystem::GetActorsByMaterialName(
|
||||
const EDirectiveUtilInclusivity Inclusivity)
|
||||
{
|
||||
const TArray<AActor*> SourceActors = SelectionMethod == Selection ? GetSelectedLevelActors() : GetAllLevelActors();
|
||||
|
||||
TArray<AStaticMeshActor*> StaticMeshActors;
|
||||
FilterStaticMeshActors(StaticMeshActors, SourceActors);
|
||||
|
||||
FilterActorsByPredicate(StaticMeshActors, FoundActors, Inclusivity, [&](AStaticMeshActor* StaticMeshActor) -> bool
|
||||
{
|
||||
const UStaticMeshComponent* StaticMeshComp = StaticMeshActor->GetStaticMeshComponent();
|
||||
if (!StaticMeshComp) { return false; }
|
||||
const UStaticMesh* Mesh = StaticMeshComp->GetStaticMesh();
|
||||
if (!Mesh) { return false; }
|
||||
|
||||
if (MaterialSource == BaseAndOverride || MaterialSource == OverrideOnly)
|
||||
{
|
||||
for (int32 i = 0; i < StaticMeshComp->GetNumMaterials(); i++)
|
||||
{
|
||||
const UMaterialInterface* Mat = StaticMeshComp->GetMaterial(i);
|
||||
if (Mat && Mat->GetName().Contains(MaterialName)) { return true; }
|
||||
}
|
||||
}
|
||||
if (MaterialSource == BaseAndOverride || MaterialSource == BaseOnly)
|
||||
{
|
||||
for (int32 i = 0; i < Mesh->GetStaticMaterials().Num(); i++)
|
||||
{
|
||||
const UMaterialInterface* Mat = Mesh->GetMaterial(i);
|
||||
if (Mat && Mat->GetName().Contains(MaterialName)) { return true; }
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
UE_LOG(LogDirectiveUtilEditor, Display, TEXT("%i actors with material %s were found."), FoundActors.Num(),
|
||||
*MaterialName);
|
||||
FilterActorsByMaterialName(SourceActors, FoundActors, MaterialName, MaterialSource, Inclusivity);
|
||||
}
|
||||
|
||||
void UDirectiveUtilEditorActorSubsystem::GetActorsByVertexCount(
|
||||
@@ -1288,11 +1624,11 @@ void UDirectiveUtilEditorActorSubsystem::GetActorsByTextureSoftReference(
|
||||
{
|
||||
if (const UMaterialExpressionTextureSample* TextureSample = Cast<UMaterialExpressionTextureSample>(Expression))
|
||||
{
|
||||
if (TextureSample->Texture == Texture) { return true; }
|
||||
if (MatchesTextureReference(TextureSample->Texture, Texture)) { return true; }
|
||||
}
|
||||
if (const UMaterialExpressionTextureObject* TextureObject = Cast<UMaterialExpressionTextureObject>(Expression))
|
||||
{
|
||||
if (TextureObject->Texture == Texture) { return true; }
|
||||
if (MatchesTextureReference(TextureObject->Texture, Texture)) { return true; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1341,8 +1677,7 @@ void UDirectiveUtilEditorActorSubsystem::GetActorsByTextureName(
|
||||
|
||||
void UDirectiveUtilEditorActorSubsystem::GetInvalidActors(TArray<AActor*>& FoundActors)
|
||||
{
|
||||
for (AActor* Actor : GetAllLevelActors()) { if (!IsValid(Actor)) { FoundActors.AddUnique(Actor); } }
|
||||
UE_LOG(LogDirectiveUtilEditor, Display, TEXT("%i invalid actors were found."), FoundActors.Num());
|
||||
FoundActors.Reset();
|
||||
}
|
||||
|
||||
void UDirectiveUtilEditorActorSubsystem::PushOverrideMaterialsToSource(UStaticMeshComponent* StaticMeshComponent)
|
||||
@@ -1361,11 +1696,14 @@ void UDirectiveUtilEditorActorSubsystem::PushOverrideMaterialsToSource(UStaticMe
|
||||
}
|
||||
|
||||
const FScopedTransaction Transaction(NSLOCTEXT("DirectiveUtilities", "PushOverrideMaterialsToSource", "Push Override Materials To Source"));
|
||||
for (int32 i = 0; i < StaticMeshComponent->GetNumMaterials(); i++)
|
||||
const TArray<TObjectPtr<UMaterialInterface>>& OverrideMaterials = StaticMeshComponent->OverrideMaterials;
|
||||
const int32 MaterialSlotCount = StaticMesh->GetStaticMaterials().Num();
|
||||
for (int32 MaterialIndex = 0; MaterialIndex < FMath::Min(OverrideMaterials.Num(), MaterialSlotCount); ++MaterialIndex)
|
||||
{
|
||||
if (UMaterialInterface* Material = StaticMeshComponent->GetMaterial(i))
|
||||
UMaterialInterface* OverrideMaterial = OverrideMaterials[MaterialIndex];
|
||||
if (OverrideMaterial && StaticMesh->GetMaterial(MaterialIndex) != OverrideMaterial)
|
||||
{
|
||||
StaticMesh->SetMaterial(i, Material);
|
||||
StaticMesh->SetMaterial(MaterialIndex, OverrideMaterial);
|
||||
}
|
||||
}
|
||||
UE_LOG(LogDirectiveUtilEditor, Display, TEXT("Materials were pushed to source for %s."), *StaticMeshComponent->GetName());
|
||||
|
||||
@@ -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
|
||||
|
||||
#include "AssetRegistry/AssetData.h"
|
||||
#include "CoreMinimal.h"
|
||||
#include "EditorAssetLibrary.h"
|
||||
#include "Types/DirectiveUtilEditorAssetTypes.h"
|
||||
@@ -44,7 +45,8 @@ public:
|
||||
|
||||
/**
|
||||
* Fixes up (and deletes) object redirectors found under the given directories, without loading every asset.
|
||||
* Equivalent to the Content Browser's "Fix Up Redirectors in Folder", but scriptable and headless-friendly.
|
||||
* Equivalent to the Content Browser's "Fix Up Redirectors in Folder". Unreal shows a completion dialog,
|
||||
* so this returns Failure in unattended sessions instead of blocking the process.
|
||||
* @param DirectoryPaths Directories to scan for redirectors. If empty, the entire registry is scanned.
|
||||
* @param OutRedirectorsProcessed [out] The number of redirectors submitted for fix-up (the engine does not report per-redirector success).
|
||||
* @return Success if the operation ran (even if nothing needed fixing), Failure otherwise (e.g. a fixup is already in progress).
|
||||
|
||||
@@ -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 UBoxComponent;
|
||||
class USphereComponent;
|
||||
class UStaticMeshActor;
|
||||
class AStaticMeshActor;
|
||||
|
||||
/**
|
||||
* DirectiveUtilEditorActorSubsystem
|
||||
@@ -44,6 +44,30 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor")
|
||||
TArray<UClass*> GetAllLevelClasses();
|
||||
|
||||
/** Aligns actor bounds to the combined minimum, center, or maximum on one axis. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Actor Layout")
|
||||
static FDirectiveUtilActorOperationResult AlignActors(
|
||||
const TArray<AActor*>& Actors,
|
||||
EDirectiveUtilActorLayoutAxis Axis,
|
||||
EDirectiveUtilActorAlignment Alignment);
|
||||
|
||||
/** Distributes actors between the outer actors using equal center spacing or equal bounds gaps. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Actor Layout")
|
||||
static FDirectiveUtilActorOperationResult DistributeActors(
|
||||
const TArray<AActor*>& Actors,
|
||||
EDirectiveUtilActorLayoutAxis Axis,
|
||||
EDirectiveUtilActorDistribution Distribution);
|
||||
|
||||
/** Traces from each actor and places its pivot or directional bounds on the hit surface. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Editor|Actor Layout", meta = (AdvancedDisplay = "bAlignToNormal"))
|
||||
static FDirectiveUtilActorOperationResult SnapActorsToSurface(
|
||||
const TArray<AActor*>& Actors,
|
||||
FVector TraceDirection,
|
||||
float MaximumDistance,
|
||||
TEnumAsByte<ECollisionChannel> TraceChannel = ECC_Visibility,
|
||||
EDirectiveUtilSurfacePlacement Placement = EDirectiveUtilSurfacePlacement::Bounds,
|
||||
bool bAlignToNormal = false);
|
||||
|
||||
//-----------------------------
|
||||
// Filters
|
||||
//-----------------------------
|
||||
@@ -679,7 +703,7 @@ public:
|
||||
* Returns a list of invalid actors.
|
||||
* @param FoundActors The list of actors that were found.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(AdvancedDisplay=1))
|
||||
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Select", meta=(DeprecatedFunction, DeprecationMessage="Actor enumeration excludes invalid actors. This node always returns an empty array."))
|
||||
void GetInvalidActors(TArray<AActor*>& FoundActors);
|
||||
|
||||
//-----------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
#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
|
||||
|
||||
@@ -13,6 +13,8 @@ public class DirectiveUtilitiesRuntime : ModuleRules
|
||||
new string[]
|
||||
{
|
||||
"Core",
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"GameplayTags",
|
||||
"NetCore",
|
||||
}
|
||||
@@ -22,11 +24,10 @@ public class DirectiveUtilitiesRuntime : ModuleRules
|
||||
PrivateDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"Slate",
|
||||
"SlateCore",
|
||||
"AIModule",
|
||||
"NavigationSystem",
|
||||
"EnhancedInput",
|
||||
"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(LogDirectiveUtilEditor);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,9 +2,92 @@
|
||||
|
||||
|
||||
#include "Libraries/DirectiveUtilFunctionLibrary.h"
|
||||
#include "Engine/World.h"
|
||||
#include "HAL/CriticalSection.h"
|
||||
#include "HAL/PlatformApplicationMisc.h"
|
||||
#include "HAL/PlatformTime.h"
|
||||
#include "Misc/App.h"
|
||||
#include "Misc/CommandLine.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)
|
||||
{
|
||||
@@ -57,6 +140,22 @@ bool UDirectiveUtilFunctionLibrary::IsRunningInEditor()
|
||||
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)
|
||||
{
|
||||
return HasCommandLineSwitch(FCommandLine::Get(), Switch);
|
||||
@@ -67,6 +166,44 @@ bool UDirectiveUtilFunctionLibrary::GetCommandLineOption(const FString& Key, FSt
|
||||
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)
|
||||
{
|
||||
if (Switch.IsEmpty())
|
||||
|
||||
@@ -27,13 +27,26 @@ FGameplayTagContainer UDirectiveUtilGameplayTagFunctionLibrary::GetTagParents(co
|
||||
|
||||
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)
|
||||
{
|
||||
const TArray<FString> Segments = GetTagSegments(Tag);
|
||||
return Segments.Num() > 0 ? Segments.Last() : FString();
|
||||
if (!Tag.IsValid())
|
||||
{
|
||||
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)
|
||||
@@ -64,10 +77,9 @@ FGameplayTagContainer UDirectiveUtilGameplayTagFunctionLibrary::GetTagDirectChil
|
||||
return DirectChildren;
|
||||
}
|
||||
|
||||
const int32 DirectChildDepth = GetTagDepth(Tag) + 1;
|
||||
for (const FGameplayTag& Child : GetTagChildren(Tag))
|
||||
{
|
||||
if (GetTagDepth(Child) == DirectChildDepth)
|
||||
if (Child.RequestDirectParent() == Tag)
|
||||
{
|
||||
DirectChildren.AddTag(Child);
|
||||
}
|
||||
@@ -77,29 +89,31 @@ FGameplayTagContainer UDirectiveUtilGameplayTagFunctionLibrary::GetTagDirectChil
|
||||
|
||||
FGameplayTag UDirectiveUtilGameplayTagFunctionLibrary::GetTagCommonAncestor(const FGameplayTag& TagA, const FGameplayTag& TagB)
|
||||
{
|
||||
const TArray<FString> SegmentsA = GetTagSegments(TagA);
|
||||
const TArray<FString> SegmentsB = GetTagSegments(TagB);
|
||||
|
||||
FString Prefix;
|
||||
for (int32 Index = 0; Index < SegmentsA.Num() && Index < SegmentsB.Num(); ++Index)
|
||||
{
|
||||
if (!SegmentsA[Index].Equals(SegmentsB[Index]))
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (!Prefix.IsEmpty())
|
||||
{
|
||||
Prefix += TEXT(".");
|
||||
}
|
||||
Prefix += SegmentsA[Index];
|
||||
}
|
||||
|
||||
if (Prefix.IsEmpty())
|
||||
if (!TagA.IsValid() || !TagB.IsValid())
|
||||
{
|
||||
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)
|
||||
@@ -109,20 +123,18 @@ FGameplayTag UDirectiveUtilGameplayTagFunctionLibrary::GetTagAtDepth(const FGame
|
||||
return FGameplayTag();
|
||||
}
|
||||
|
||||
const TArray<FString> Segments = GetTagSegments(Tag);
|
||||
if (Depth >= Segments.Num())
|
||||
const int32 TagDepth = GetTagDepth(Tag);
|
||||
if (Depth >= TagDepth)
|
||||
{
|
||||
return Tag;
|
||||
}
|
||||
|
||||
FString Prefix = Segments[0];
|
||||
for (int32 Index = 1; Index < Depth; ++Index)
|
||||
FGameplayTag Ancestor = Tag;
|
||||
for (int32 CurrentDepth = TagDepth; CurrentDepth > Depth; --CurrentDepth)
|
||||
{
|
||||
Prefix += TEXT(".");
|
||||
Prefix += Segments[Index];
|
||||
Ancestor = Ancestor.RequestDirectParent();
|
||||
}
|
||||
// An ancestor of a registered tag is always registered itself (parents auto-register).
|
||||
return FGameplayTag::RequestGameplayTag(FName(*Prefix), false);
|
||||
return Ancestor;
|
||||
}
|
||||
|
||||
FGameplayTagContainer UDirectiveUtilGameplayTagFunctionLibrary::GetTagSiblings(const FGameplayTag& Tag)
|
||||
|
||||
@@ -77,8 +77,11 @@ EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::AddInputMappingC
|
||||
|
||||
if (FailedIndices.Num() > 0)
|
||||
{
|
||||
FString FailedIndicesStr = FString::JoinBy(FailedIndices, TEXT(", "), [](const int32 Index) { return FString::Printf(TEXT("%d"), Index); });
|
||||
UE_LOGFMT(LogDirectiveUtil, Warning, "{FailedIndicies} Input Mapping Contexts failed to load and were not added! The failed indexes are [{FailedIndicieIndexes}]", FailedIndices.Num(), FailedIndicesStr);
|
||||
const FString FailedIndicesString = FString::JoinBy(
|
||||
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())
|
||||
@@ -104,7 +107,7 @@ EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::RemoveInputMappi
|
||||
AController* PlayerController,
|
||||
const TArray<TSoftObjectPtr<UInputMappingContext>>& Contexts)
|
||||
{
|
||||
if (Contexts.IsEmpty()) { return EDirectiveUtilSuccessStatus::Failure;; }
|
||||
if (Contexts.IsEmpty()) { return EDirectiveUtilSuccessStatus::Failure; }
|
||||
|
||||
UEnhancedInputLocalPlayerSubsystem* EnhancedInput;
|
||||
const bool bEnhancedInputRetrievedFromController = TryGetEnhancedInputSubsystemFromController(PlayerController, EnhancedInput);
|
||||
@@ -118,10 +121,11 @@ EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::RemoveInputMappi
|
||||
for (int32 Index = 0; Index < Contexts.Num(); ++Index)
|
||||
{
|
||||
const TSoftObjectPtr<UInputMappingContext>& Context = Contexts[Index];
|
||||
if (const UInputMappingContext* MappingContext = Context.LoadSynchronous())
|
||||
if (const UInputMappingContext* MappingContext = Context.Get())
|
||||
{
|
||||
EnhancedInput->RemoveMappingContext(MappingContext);
|
||||
} else
|
||||
}
|
||||
else
|
||||
{
|
||||
FailedIndices.Add(Index);
|
||||
}
|
||||
@@ -129,8 +133,11 @@ EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::RemoveInputMappi
|
||||
|
||||
if (FailedIndices.Num() > 0)
|
||||
{
|
||||
FString FailedIndicesStr = FString::JoinBy(FailedIndices, TEXT(", "), [](const int32 Index) { return FString::Printf(TEXT("%d"), Index); });
|
||||
UE_LOGFMT(LogDirectiveUtil, Warning, "{FailedIndicies} Input Mapping Contexts failed to load and were not removed! The failed indexes are [{FailedIndicieIndexes}]", FailedIndices.Num(), FailedIndicesStr);
|
||||
const FString FailedIndicesString = FString::JoinBy(
|
||||
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())
|
||||
@@ -143,28 +150,27 @@ EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::RemoveInputMappi
|
||||
}
|
||||
|
||||
EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::SwapInputMappingContexts(
|
||||
AController* PlayerController,
|
||||
const TSoftObjectPtr<UInputMappingContext> PreviousContext,
|
||||
const TSoftObjectPtr<UInputMappingContext> NewContext,
|
||||
const int32 Priority,
|
||||
const bool bUsePreviousPriority)
|
||||
AController* PlayerController,
|
||||
const TSoftObjectPtr<UInputMappingContext> PreviousContext,
|
||||
const TSoftObjectPtr<UInputMappingContext> NewContext,
|
||||
const int32 Priority,
|
||||
const bool bUsePreviousPriority)
|
||||
{
|
||||
const UInputMappingContext* LoadedPreviousMappingContext = PreviousContext.LoadSynchronous();
|
||||
const UInputMappingContext* LoadedNewMappingContext = NewContext.LoadSynchronous();
|
||||
|
||||
if (!LoadedPreviousMappingContext || !LoadedNewMappingContext)
|
||||
{
|
||||
UE_LOGFMT(LogDirectiveUtil, Warning, "Both the previous and new input mapping contexts must be valid.");
|
||||
return EDirectiveUtilSuccessStatus::Failure;
|
||||
}
|
||||
|
||||
UEnhancedInputLocalPlayerSubsystem* EnhancedInput;
|
||||
if (!TryGetEnhancedInputSubsystemFromController(PlayerController, EnhancedInput))
|
||||
{
|
||||
return EDirectiveUtilSuccessStatus::Failure;
|
||||
}
|
||||
|
||||
if (int32 PreviousPriority; EnhancedInput->HasMappingContext(LoadedPreviousMappingContext, PreviousPriority))
|
||||
const 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;
|
||||
EnhancedInput->RemoveMappingContext(LoadedPreviousMappingContext);
|
||||
@@ -174,7 +180,7 @@ EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::SwapInputMapping
|
||||
else
|
||||
{
|
||||
EnhancedInput->AddMappingContext(LoadedNewMappingContext, Priority);
|
||||
UE_LOGFMT(LogDirectiveUtil, Warning, "Previous input mapping context {PreviousContext} not found. New context {NewContext} added at priority {BackupPriority}.", LoadedPreviousMappingContext->GetName(), LoadedNewMappingContext->GetName(), Priority);
|
||||
UE_LOGFMT(LogDirectiveUtil, Verbose, "Previous input mapping context was not active. New context {NewContext} added at priority {BackupPriority}.", LoadedNewMappingContext->GetName(), Priority);
|
||||
}
|
||||
|
||||
return EDirectiveUtilSuccessStatus::Success;
|
||||
@@ -189,7 +195,7 @@ UEnhancedInputLocalPlayerSubsystem* UDirectiveUtilInputFunctionLibrary::GetEnhan
|
||||
|
||||
bool UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(AController* PlayerController, TSoftObjectPtr<UInputMappingContext> Context)
|
||||
{
|
||||
const UInputMappingContext* MappingContext = Context.LoadSynchronous();
|
||||
const UInputMappingContext* MappingContext = Context.Get();
|
||||
if (!MappingContext)
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -179,7 +179,6 @@ void UDirectiveUtilMapFunctionLibrary::GenericMap_Append(
|
||||
return;
|
||||
}
|
||||
|
||||
// Appending a map onto itself is a no-op; bail before AddPair can reallocate under the source pointers.
|
||||
if (TargetMap == SourceMap)
|
||||
{
|
||||
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 "HAL/FileManager.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "PlatformFeatures.h"
|
||||
#include "SaveGameSystem.h"
|
||||
|
||||
#include <ctime>
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -24,11 +28,51 @@ namespace
|
||||
{
|
||||
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> SlotNames;
|
||||
if (ISaveGameSystem* SaveSystem = GetSaveGameSystem())
|
||||
{
|
||||
if (SaveSystem->GetSaveGameNames(SlotNames, 0))
|
||||
{
|
||||
return SlotNames;
|
||||
}
|
||||
}
|
||||
|
||||
TArray<FString> Files;
|
||||
IFileManager::Get().FindFiles(Files, *(GetSaveGamesDirectory() / TEXT("*.sav")), true, false);
|
||||
@@ -49,13 +93,21 @@ bool UDirectiveUtilSaveGameFunctionLibrary::GetSaveSlotTimestamp(const FString&
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ISaveGameSystem* SaveSystem = GetSaveGameSystem())
|
||||
{
|
||||
if (!SaveSystem->DoesSaveGameExist(*SlotName, 0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const FDateTime Timestamp = IFileManager::Get().GetTimeStamp(*GetSaveSlotFilePath(SlotName));
|
||||
if (Timestamp == FDateTime::MinValue())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OutTimestamp = Timestamp + (FDateTime::Now() - FDateTime::UtcNow());
|
||||
OutTimestamp = ConvertUtcFileTimeToLocal(Timestamp);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -98,10 +150,30 @@ bool UDirectiveUtilSaveGameFunctionLibrary::DeleteSaveSlot(const FString& SlotNa
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -14,6 +14,80 @@ namespace
|
||||
const FTCHARToUTF8 Converter(*String, String.Len());
|
||||
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)
|
||||
@@ -219,50 +293,27 @@ TArray<FString> UDirectiveUtilStringFunctionLibrary::GetSortedStringArray(const
|
||||
|
||||
int32 UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(const FString& A, const FString& B, const bool bCaseSensitive)
|
||||
{
|
||||
const FString StringA = bCaseSensitive ? A : A.ToLower();
|
||||
const FString StringB = bCaseSensitive ? B : B.ToLower();
|
||||
const int32 LenA = StringA.Len();
|
||||
const int32 LenB = StringB.Len();
|
||||
|
||||
if (LenA == 0) { return LenB; }
|
||||
if (LenB == 0) { return LenA; }
|
||||
|
||||
const FString NormalizedA = bCaseSensitive ? FString() : A.ToLower();
|
||||
const FString NormalizedB = bCaseSensitive ? FString() : B.ToLower();
|
||||
const FStringView ViewA = bCaseSensitive ? FStringView(A) : FStringView(NormalizedA);
|
||||
const FStringView ViewB = bCaseSensitive ? FStringView(B) : FStringView(NormalizedB);
|
||||
TArray<int32> PreviousRow;
|
||||
TArray<int32> CurrentRow;
|
||||
PreviousRow.SetNumUninitialized(LenB + 1);
|
||||
CurrentRow.SetNumUninitialized(LenB + 1);
|
||||
|
||||
for (int32 ColumnIndex = 0; ColumnIndex <= LenB; ++ColumnIndex)
|
||||
{
|
||||
PreviousRow[ColumnIndex] = ColumnIndex;
|
||||
}
|
||||
|
||||
for (int32 RowIndex = 1; RowIndex <= LenA; ++RowIndex)
|
||||
{
|
||||
CurrentRow[0] = RowIndex;
|
||||
for (int32 ColumnIndex = 1; ColumnIndex <= LenB; ++ColumnIndex)
|
||||
{
|
||||
const int32 SubstitutionCost = (StringA[RowIndex - 1] == StringB[ColumnIndex - 1]) ? 0 : 1;
|
||||
CurrentRow[ColumnIndex] = FMath::Min3(
|
||||
PreviousRow[ColumnIndex] + 1,
|
||||
CurrentRow[ColumnIndex - 1] + 1,
|
||||
PreviousRow[ColumnIndex - 1] + SubstitutionCost);
|
||||
}
|
||||
Exchange(PreviousRow, CurrentRow);
|
||||
}
|
||||
|
||||
return PreviousRow[LenB];
|
||||
return CalculateLevenshteinDistance(ViewA, ViewB, PreviousRow, CurrentRow);
|
||||
}
|
||||
|
||||
float UDirectiveUtilStringFunctionLibrary::GetStringSimilarity(const FString& A, const FString& B, const bool bCaseSensitive)
|
||||
{
|
||||
const int32 MaxLength = FMath::Max(A.Len(), B.Len());
|
||||
if (MaxLength == 0)
|
||||
{
|
||||
return 1.0f;
|
||||
}
|
||||
const int32 Distance = GetLevenshteinDistance(A, B, bCaseSensitive);
|
||||
return 1.0f - (static_cast<float>(Distance) / static_cast<float>(MaxLength));
|
||||
const FString NormalizedA = bCaseSensitive ? FString() : A.ToLower();
|
||||
const FString NormalizedB = bCaseSensitive ? FString() : B.ToLower();
|
||||
TArray<int32> PreviousRow;
|
||||
TArray<int32> CurrentRow;
|
||||
return CalculateStringSimilarity(
|
||||
bCaseSensitive ? FStringView(A) : FStringView(NormalizedA),
|
||||
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)
|
||||
@@ -394,28 +445,96 @@ int32 UDirectiveUtilStringFunctionLibrary::Crc32Bytes(const TArray<uint8>& Bytes
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
OutSimilarity = 0.0f;
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
BestIndex = Index;
|
||||
OutSimilarity = Similarity;
|
||||
if (Similarity == 1.0f)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
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()
|
||||
{
|
||||
UObject* LoadedAsset = StreamableHandle.IsValid() ? StreamableHandle->GetLoadedAsset() : nullptr;
|
||||
UObject* LoadedAsset = SoftAsset.Get();
|
||||
if (LoadedAsset)
|
||||
{
|
||||
Completed.Broadcast(LoadedAsset);
|
||||
@@ -120,7 +120,7 @@ void UDirectiveUtilTask_AsyncLoadClass::Activate()
|
||||
|
||||
void UDirectiveUtilTask_AsyncLoadClass::OnLoaded()
|
||||
{
|
||||
UClass* LoadedClass = StreamableHandle.IsValid() ? Cast<UClass>(StreamableHandle->GetLoadedAsset()) : nullptr;
|
||||
UClass* LoadedClass = SoftClass.Get();
|
||||
if (LoadedClass)
|
||||
{
|
||||
Completed.Broadcast(LoadedClass);
|
||||
@@ -157,8 +157,6 @@ UDirectiveUtilTask_AsyncLoadAssets* UDirectiveUtilTask_AsyncLoadAssets::AsyncLoa
|
||||
|
||||
void UDirectiveUtilTask_AsyncLoadAssets::Activate()
|
||||
{
|
||||
// Unset references are filtered out of the request but keep their null slots in the output;
|
||||
// duplicates are requested once and resolved per slot.
|
||||
TArray<FSoftObjectPath> PathsToLoad;
|
||||
for (const TSoftObjectPtr<UObject>& SoftAsset : SoftAssets)
|
||||
{
|
||||
@@ -170,7 +168,6 @@ void UDirectiveUtilTask_AsyncLoadAssets::Activate()
|
||||
|
||||
if (PathsToLoad.Num() == 0)
|
||||
{
|
||||
// Nothing to request; an empty request list is an error path in the streamable manager.
|
||||
OnLoaded();
|
||||
return;
|
||||
}
|
||||
@@ -194,8 +191,6 @@ void UDirectiveUtilTask_AsyncLoadAssets::Activate()
|
||||
return;
|
||||
}
|
||||
|
||||
// Binding fails when the load already finished (all assets were in memory); complete directly,
|
||||
// with the guard keeping the broadcast exactly once.
|
||||
if (!StreamableHandle->BindUpdateDelegate(FStreamableUpdateDelegate::CreateUObject(this, &UDirectiveUtilTask_AsyncLoadAssets::OnUpdate)))
|
||||
{
|
||||
OnLoaded();
|
||||
|
||||
@@ -25,11 +25,28 @@ UDirectiveUtilTask_Delay* UDirectiveUtilTask_Delay::CancellableDelay(UObject* Wo
|
||||
|
||||
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()
|
||||
@@ -40,11 +57,14 @@ void UDirectiveUtilTask_Delay::Activate()
|
||||
if (!World)
|
||||
{
|
||||
UE_LOG(LogDirectiveUtil, Warning, TEXT("Cancellable Delay failed to activate. World is null."));
|
||||
bFinished = true;
|
||||
SetReadyToDestroy();
|
||||
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);
|
||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Cancellable Delay started for %f seconds."), ClampedDuration);
|
||||
}
|
||||
@@ -52,6 +72,10 @@ void UDirectiveUtilTask_Delay::Activate()
|
||||
void UDirectiveUtilTask_Delay::OnDelayComplete()
|
||||
{
|
||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Cancellable Delay completed."));
|
||||
Completed.Broadcast();
|
||||
if (ShouldBroadcastDelegates())
|
||||
{
|
||||
Completed.Broadcast();
|
||||
}
|
||||
bFinished = true;
|
||||
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 "Engine/World.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
#include "NavigationSystem.h"
|
||||
#include "Navigation/PathFollowingComponent.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(
|
||||
UObject* WorldContextObject,
|
||||
@@ -22,9 +35,9 @@ UDirectiveUtilTask_MoveToLocation* UDirectiveUtilTask_MoveToLocation::MoveToLoca
|
||||
UDirectiveUtilTask_MoveToLocation* Action = NewObject<UDirectiveUtilTask_MoveToLocation>();
|
||||
Action->Controller = Controller;
|
||||
Action->Destination = Destination;
|
||||
Action->AcceptanceRadius = AcceptanceRadius;
|
||||
Action->AcceptanceRadius = GetNonNegativeFiniteValue(AcceptanceRadius);
|
||||
Action->bDebugLineTrace = bDebugLineTrace;
|
||||
Action->StuckThreshold = StuckThreshold;
|
||||
Action->StuckThreshold = GetNonNegativeFiniteValue(StuckThreshold);
|
||||
Action->bCheckStuckMovement = bCheckStuckMovement;
|
||||
|
||||
if (WorldContextObject)
|
||||
@@ -38,27 +51,45 @@ UDirectiveUtilTask_MoveToLocation* UDirectiveUtilTask_MoveToLocation::MoveToLoca
|
||||
|
||||
void UDirectiveUtilTask_MoveToLocation::EndTask()
|
||||
{
|
||||
if (IsValid(Controller))
|
||||
{
|
||||
Controller->StopMovement();
|
||||
}
|
||||
ExecuteCompleted(false);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
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)
|
||||
{
|
||||
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);
|
||||
@@ -67,7 +98,7 @@ void UDirectiveUtilTask_MoveToLocation::Activate()
|
||||
if (bDebugLineTrace)
|
||||
{
|
||||
DrawDebugLine(
|
||||
Controller->GetWorld(),
|
||||
World,
|
||||
Destination + FVector(0, 0, 100),
|
||||
Destination,
|
||||
FColor::Green,
|
||||
@@ -81,18 +112,18 @@ void UDirectiveUtilTask_MoveToLocation::Activate()
|
||||
|
||||
void UDirectiveUtilTask_MoveToLocation::CheckMoveToLocation()
|
||||
{
|
||||
|
||||
if(!Controller || !Controller->GetPawn())
|
||||
APawn* Pawn = IsValid(Controller) ? Controller->GetPawn() : nullptr;
|
||||
if (!IsValid(Pawn) || !TimerWorld.IsValid())
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
if (FVector::Dist(CurrentLocation, Destination) < AcceptanceRadius)
|
||||
if (IsWithinAcceptanceRadius(CurrentLocation, Destination, AcceptanceRadius))
|
||||
{
|
||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Controller has moved to location."));
|
||||
ExecuteCompleted(true);
|
||||
@@ -103,16 +134,17 @@ void UDirectiveUtilTask_MoveToLocation::CheckMoveToLocation()
|
||||
if (!PathFollowing || PathFollowing->GetStatus() == EPathFollowingStatus::Idle)
|
||||
{
|
||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Path following has stopped. Completing move to location."));
|
||||
ExecuteCompleted(FVector::Dist(CurrentLocation, Destination) < AcceptanceRadius);
|
||||
ExecuteCompleted(IsWithinAcceptanceRadius(CurrentLocation, Destination, AcceptanceRadius));
|
||||
}
|
||||
}
|
||||
|
||||
void UDirectiveUtilTask_MoveToLocation::CheckStuckMovement()
|
||||
{
|
||||
if(!Controller || !Controller->GetPawn())
|
||||
const APawn* Pawn = IsValid(Controller) ? Controller->GetPawn() : nullptr;
|
||||
if (!IsValid(Pawn) || !TimerWorld.IsValid())
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -133,13 +165,14 @@ void UDirectiveUtilTask_MoveToLocation::ExecuteCompleted(const bool bSuccess)
|
||||
}
|
||||
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);
|
||||
Controller->GetWorld()->GetTimerManager().ClearTimer(StuckTimerHandle);
|
||||
World->GetTimerManager().ClearTimer(TimerHandle);
|
||||
World->GetTimerManager().ClearTimer(StuckTimerHandle);
|
||||
}
|
||||
TimerWorld.Reset();
|
||||
|
||||
Completed.Broadcast(bSuccess);
|
||||
|
||||
@@ -160,8 +193,8 @@ UDirectiveUtilTask_MoveToActor* UDirectiveUtilTask_MoveToActor::MoveToActor(
|
||||
UDirectiveUtilTask_MoveToActor* Action = NewObject<UDirectiveUtilTask_MoveToActor>();
|
||||
Action->Controller = Controller;
|
||||
Action->Goal = Goal;
|
||||
Action->AcceptanceRadius = AcceptanceRadius;
|
||||
Action->StuckThreshold = StuckThreshold;
|
||||
Action->AcceptanceRadius = GetNonNegativeFiniteValue(AcceptanceRadius);
|
||||
Action->StuckThreshold = GetNonNegativeFiniteValue(StuckThreshold);
|
||||
Action->bCheckStuckMovement = bCheckStuckMovement;
|
||||
|
||||
if (WorldContextObject)
|
||||
@@ -174,27 +207,45 @@ UDirectiveUtilTask_MoveToActor* UDirectiveUtilTask_MoveToActor::MoveToActor(
|
||||
|
||||
void UDirectiveUtilTask_MoveToActor::EndTask()
|
||||
{
|
||||
if (IsValid(Controller))
|
||||
{
|
||||
Controller->StopMovement();
|
||||
}
|
||||
ExecuteCompleted(false);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
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)
|
||||
{
|
||||
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);
|
||||
@@ -203,10 +254,11 @@ void UDirectiveUtilTask_MoveToActor::Activate()
|
||||
|
||||
void UDirectiveUtilTask_MoveToActor::CheckMoveToActor()
|
||||
{
|
||||
if(!Controller || !Controller->GetPawn())
|
||||
APawn* Pawn = IsValid(Controller) ? Controller->GetPawn() : nullptr;
|
||||
if (!IsValid(Pawn) || !TimerWorld.IsValid())
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -219,10 +271,10 @@ void UDirectiveUtilTask_MoveToActor::CheckMoveToActor()
|
||||
|
||||
// The goal can move, so its location is re-read every poll.
|
||||
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));
|
||||
|
||||
if (FVector::Dist(CurrentLocation, GoalLocation) < AcceptanceRadius)
|
||||
if (IsWithinAcceptanceRadius(CurrentLocation, GoalLocation, AcceptanceRadius))
|
||||
{
|
||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Controller has moved to actor."));
|
||||
ExecuteCompleted(true);
|
||||
@@ -233,16 +285,17 @@ void UDirectiveUtilTask_MoveToActor::CheckMoveToActor()
|
||||
if (!PathFollowing || PathFollowing->GetStatus() == EPathFollowingStatus::Idle)
|
||||
{
|
||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Path following has stopped. Completing move to actor."));
|
||||
ExecuteCompleted(FVector::Dist(CurrentLocation, GoalLocation) < AcceptanceRadius);
|
||||
ExecuteCompleted(IsWithinAcceptanceRadius(CurrentLocation, GoalLocation, AcceptanceRadius));
|
||||
}
|
||||
}
|
||||
|
||||
void UDirectiveUtilTask_MoveToActor::CheckStuckMovement()
|
||||
{
|
||||
if(!Controller || !Controller->GetPawn())
|
||||
const APawn* Pawn = IsValid(Controller) ? Controller->GetPawn() : nullptr;
|
||||
if (!IsValid(Pawn) || !TimerWorld.IsValid())
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -263,13 +316,14 @@ void UDirectiveUtilTask_MoveToActor::ExecuteCompleted(const bool bSuccess)
|
||||
}
|
||||
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);
|
||||
Controller->GetWorld()->GetTimerManager().ClearTimer(StuckTimerHandle);
|
||||
World->GetTimerManager().ClearTimer(TimerHandle);
|
||||
World->GetTimerManager().ClearTimer(StuckTimerHandle);
|
||||
}
|
||||
TimerWorld.Reset();
|
||||
|
||||
Completed.Broadcast(bSuccess);
|
||||
|
||||
|
||||
@@ -51,7 +51,25 @@ public:
|
||||
* @param TargetArray - The array to remove duplicates from.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Remove Duplicates", CompactNodeTitle = "REMOVE DUPLICATES", ArrayParm = "TargetArray"), Category="Directive Utilities|Array")
|
||||
static void Array_RemoveDuplicates(const TArray<int32>& TargetArray);
|
||||
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.
|
||||
@@ -106,7 +124,7 @@ public:
|
||||
* @returns True if an element was removed.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Pop", ArrayParm = "TargetArray", ArrayTypeDependentParams = "OutItem"), Category="Directive Utilities|Array")
|
||||
static bool Array_Pop(const TArray<int32>& TargetArray, int32& OutItem);
|
||||
static bool Array_Pop(UPARAM(ref) TArray<int32>& TargetArray, int32& OutItem);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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).
|
||||
@@ -125,7 +143,26 @@ public:
|
||||
* @returns True if the index was valid and an element was removed.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Remove At Swap", ArrayParm = "TargetArray"), Category="Directive Utilities|Array")
|
||||
static bool Array_RemoveAtSwap(const TArray<int32>& TargetArray, const int32 Index);
|
||||
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.
|
||||
@@ -143,7 +180,7 @@ public:
|
||||
* @param Shift - The number of positions to rotate. Positive rotates toward the end; negative toward the start.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Rotate", ArrayParm = "TargetArray"), Category="Directive Utilities|Array")
|
||||
static void Array_Rotate(const TArray<int32>& TargetArray, const int32 Shift);
|
||||
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.
|
||||
@@ -173,6 +210,72 @@ public:
|
||||
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);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -182,6 +285,8 @@ public:
|
||||
static int32 GenericArray_NextIndex(const void* TargetArray, const FArrayProperty* ArrayProperty, int32 Index, bool bLoop);
|
||||
static int32 GenericArray_PreviousIndex(const void* TargetArray, const FArrayProperty* ArrayProperty, int32 Index, bool bLoop);
|
||||
static void GenericArray_RemoveDuplicates(void* TargetArray, const FArrayProperty* ArrayProperty);
|
||||
static 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_GetFirstItem(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_PopFirst(void* TargetArray, const FArrayProperty* ArrayProperty, void* OutItemPtr);
|
||||
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_Rotate(void* TargetArray, const FArrayProperty* ArrayProperty, int32 Shift);
|
||||
static void GenericArray_GetDistinct(const void* TargetArray, const FArrayProperty* TargetArrayProperty, void* OutArray, const FArrayProperty* OutArrayProperty);
|
||||
static int32 GenericArray_CountOccurrences(const void* TargetArray, const FArrayProperty* ArrayProperty, const void* ItemToCount);
|
||||
static bool GenericArray_GetMostCommon(const void* TargetArray, const FArrayProperty* ArrayProperty, void* OutItemPtr, int32* OutCount);
|
||||
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.
|
||||
@@ -258,6 +368,78 @@ public:
|
||||
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)
|
||||
{
|
||||
Stack.MostRecentProperty = nullptr;
|
||||
@@ -565,6 +747,70 @@ public:
|
||||
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)
|
||||
{
|
||||
Stack.MostRecentProperty = nullptr;
|
||||
@@ -713,4 +959,179 @@ public:
|
||||
P_NATIVE_END;
|
||||
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 "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "Types/DirectiveUtilTypes.h"
|
||||
#include "DirectiveUtilFunctionLibrary.generated.h"
|
||||
|
||||
/**
|
||||
@@ -46,14 +47,14 @@ public:
|
||||
* Get the content from the clipboard as FText.
|
||||
* @returns The text from the clipboard.
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Clipboard" )
|
||||
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Clipboard" )
|
||||
static FText GetTextFromClipboard();
|
||||
|
||||
/**
|
||||
* Get the content from the clipboard as an FString.
|
||||
* @returns The content from the clipboard as a string.
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Clipboard" )
|
||||
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Clipboard" )
|
||||
static FString GetStringFromClipboard();
|
||||
|
||||
/**
|
||||
@@ -77,6 +78,28 @@ public:
|
||||
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Utility")
|
||||
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
|
||||
* process command line. Matching is case-insensitive.
|
||||
@@ -96,6 +119,24 @@ public:
|
||||
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Utility")
|
||||
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. */
|
||||
static bool HasCommandLineSwitch(const TCHAR* CommandLine, const FString& Switch);
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ public:
|
||||
* @param Tag - The tag to read.
|
||||
* @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);
|
||||
|
||||
/**
|
||||
@@ -65,7 +65,7 @@ public:
|
||||
* @param Tag - The tag to read.
|
||||
* @returns A container of the tag's descendants, or an empty container for an invalid tag.
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags", meta = (BlueprintThreadSafe))
|
||||
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags")
|
||||
static FGameplayTagContainer GetTagChildren(const FGameplayTag& Tag);
|
||||
|
||||
/**
|
||||
@@ -73,7 +73,7 @@ public:
|
||||
* @param Tag - The tag to read.
|
||||
* @returns A container of the tag's direct children, or an empty container for an invalid tag.
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags", meta = (BlueprintThreadSafe))
|
||||
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags")
|
||||
static FGameplayTagContainer GetTagDirectChildren(const FGameplayTag& Tag);
|
||||
|
||||
/**
|
||||
@@ -110,7 +110,7 @@ public:
|
||||
* @param Tag - The tag to test.
|
||||
* @returns True if the tag is valid and has no registered descendants; false for an invalid tag.
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags", meta = (BlueprintThreadSafe))
|
||||
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags")
|
||||
static bool IsLeafTag(const FGameplayTag& Tag);
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,7 +36,7 @@ public:
|
||||
/**
|
||||
* Remove multiple Input Mapping Contexts.
|
||||
* @param PlayerController The player controller to remove the contexts from. Will attempt to get the LocalPlayer from the controller.
|
||||
* @param Contexts The contexts to remove.
|
||||
* @param Contexts Loaded contexts to remove. This function does not load missing assets.
|
||||
* @returns Returns Success if the contexts were successfully removed.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Input", meta=(ExpandEnumAsExecs="ReturnValue", DefaultToSelf="PlayerController"))
|
||||
@@ -45,17 +45,17 @@ public:
|
||||
const TArray<TSoftObjectPtr<UInputMappingContext>>& Contexts);
|
||||
|
||||
/**
|
||||
* Swap a designated Input Mapping Context with a new one.
|
||||
* If the previous context is found, it will be removed and the new context will be added.
|
||||
* If the previous context is not found, the new context will be added at the specified priority.
|
||||
* @param PlayerController The player controller to swap the contexts on. Will attempt to get the LocalPlayer from the controller.
|
||||
* @param PreviousContext The context to swap out.
|
||||
* @param NewContext The context to swap in.
|
||||
* @param Priority The priority to set the new context to.
|
||||
* @param bUsePreviousPriority Whether to use the previous context's priority when adding the new context.
|
||||
* @returns Returns Success if the contexts were successfully swapped.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Input", meta=(ExpandEnumAsExecs="ReturnValue", DefaultToSelf="PlayerController"))
|
||||
* Swap a designated Input Mapping Context with a new one.
|
||||
* If the previous context is found, it will be removed and the new context will be added.
|
||||
* If the previous context is not found, the new context will be added at the specified priority.
|
||||
* @param PlayerController The player controller to swap the contexts on. Will attempt to get the LocalPlayer from the controller.
|
||||
* @param PreviousContext The context to swap out.
|
||||
* @param NewContext The context to swap in. This asset is loaded synchronously when needed.
|
||||
* @param Priority The priority to set the new context to.
|
||||
* @param bUsePreviousPriority Whether to use the previous context's priority when adding the new context.
|
||||
* @returns Returns Success if the contexts were successfully swapped.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Input", meta=(ExpandEnumAsExecs="ReturnValue", DefaultToSelf="PlayerController"))
|
||||
static EDirectiveUtilSuccessStatus SwapInputMappingContexts(
|
||||
AController* PlayerController,
|
||||
TSoftObjectPtr<UInputMappingContext> PreviousContext,
|
||||
@@ -74,7 +74,7 @@ public:
|
||||
/**
|
||||
* Returns whether the given input mapping context is currently active on the controller.
|
||||
* @param PlayerController - The player controller to query.
|
||||
* @param Context - The input mapping context to check.
|
||||
* @param Context - The loaded input mapping context to check. This function does not load missing assets.
|
||||
* @returns True if the context is currently applied.
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Input", meta=(DefaultToSelf="PlayerController"))
|
||||
|
||||
@@ -67,7 +67,7 @@ public:
|
||||
* @param SourceMap - The map to copy from.
|
||||
* @param bOverwriteExisting - If true, keys already present in TargetMap are overwritten with SourceMap's values.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Append", CompactNodeTitle = "APPEND", MapParam = "TargetMap|SourceMap"), Category="Directive Utilities|Map")
|
||||
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);
|
||||
|
||||
/*~
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/SplineComponent.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "Types/DirectiveUtilMathTypes.h"
|
||||
#include "DirectiveUtilMathFunctionLibrary.generated.h"
|
||||
@@ -18,6 +19,7 @@ class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilMathFunctionLibrary : public U
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
static constexpr int32 MaximumGeneratedElementCount = 1000000;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param A - The first 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")
|
||||
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.
|
||||
* @note These are the Penner easing curves the engine's built-in "Ease" node (EEasingFunc) does not provide.
|
||||
* For Sinusoidal/Exponential/Circular/power easings, use the engine's "Ease" node instead.
|
||||
* @param Alpha - The input alpha. Clamped to the [0, 1] range.
|
||||
* @param EaseType - The easing curve to apply.
|
||||
* @returns The eased alpha. Note that Back and Elastic curves intentionally overshoot the [0, 1] range.
|
||||
* @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")
|
||||
static float EaseAlpha(float Alpha, EDirectiveUtilEaseType EaseType);
|
||||
@@ -101,6 +592,39 @@ public:
|
||||
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);
|
||||
|
||||
/**
|
||||
* 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,
|
||||
* 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
|
||||
* two-digit padding after the first ("1h 03m 05s", "2d 04h", "45s"). With bIncludeSeconds
|
||||
* false the seconds unit is dropped and sub-minute durations return "0m". Negative input gets
|
||||
* a leading minus sign; non-finite input returns "0s". Output is English-only.
|
||||
* 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 bIncludeSeconds - Whether to include the seconds unit.
|
||||
* @returns The formatted duration text.
|
||||
@@ -224,9 +748,59 @@ public:
|
||||
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
|
||||
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.
|
||||
* 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.
|
||||
* @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.
|
||||
* @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.
|
||||
*/
|
||||
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);
|
||||
|
||||
/** 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
|
||||
* 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.
|
||||
* Slot operations accept flat file names so they behave consistently across platform save backends.
|
||||
*/
|
||||
UCLASS()
|
||||
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilSaveGameFunctionLibrary : public UBlueprintFunctionLibrary
|
||||
@@ -22,9 +23,10 @@ class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilSaveGameFunctionLibrary : publ
|
||||
public:
|
||||
|
||||
/**
|
||||
* Returns the names of all existing save slots in the project's default save directory.
|
||||
* @note This enumerates the engine's default file-based save directory (Saved/SaveGames); it does not
|
||||
* cover platform-specific save systems (e.g. console storage).
|
||||
* Returns the names of all existing save slots known to the engine save game system.
|
||||
* Uses ISaveGameSystem::GetSaveGameNames so the result matches DoesSaveSlotExist /
|
||||
* 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).
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|SaveGame")
|
||||
@@ -34,7 +36,7 @@ public:
|
||||
* Returns the last-modified timestamp of a save slot, if it exists.
|
||||
* @param SlotName - The save slot name.
|
||||
* @param OutTimestamp - [out] The slot's last-modified time (local), or a default time if it does not exist.
|
||||
* Converted from the file system's UTC timestamp to local time.
|
||||
* Converted from the file system's UTC timestamp using the timezone rules for that instant.
|
||||
* @returns True if the slot exists.
|
||||
*/
|
||||
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.
|
||||
* Fails without mutating anything unless both names are valid, the names differ, the old slot
|
||||
* exists, and the new slot does not. On failure the original slot is never lost. Goes through
|
||||
* the engine's save game system, so unlike enumeration it also works on platform save backends.
|
||||
* exists, and the new slot does not. Case-only renames (Slot -> slot) rewrite the existing
|
||||
* 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 NewSlotName - The new save slot name.
|
||||
* @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
|
||||
* 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.
|
||||
* @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
|
||||
* (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 Replacement - Optional single-character replacement for stripped characters.
|
||||
* @returns The sanitized file name.
|
||||
|
||||
@@ -21,6 +21,6 @@ public:
|
||||
* Returns true if the provided text is not empty.
|
||||
* @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);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintAsyncActionBase.h"
|
||||
#include "Tasks/DirectiveUtilAsyncActionBase.h"
|
||||
#include "UObject/SoftObjectPtr.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.
|
||||
*/
|
||||
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()
|
||||
|
||||
@@ -71,7 +71,7 @@ protected:
|
||||
* Asynchronously loads a soft class reference and broadcasts the loaded class, with a cancel option.
|
||||
*/
|
||||
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Load Class"))
|
||||
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_AsyncLoadClass : public UBlueprintAsyncActionBase
|
||||
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_AsyncLoadClass : public UDirectiveUtilAsyncActionBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
@@ -124,7 +124,7 @@ protected:
|
||||
* loaded assets, with progress updates and a cancel option.
|
||||
*/
|
||||
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Load Assets"))
|
||||
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_AsyncLoadAssets : public UBlueprintAsyncActionBase
|
||||
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_AsyncLoadAssets : public UDirectiveUtilAsyncActionBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintAsyncActionBase.h"
|
||||
#include "Tasks/DirectiveUtilAsyncActionBase.h"
|
||||
#include "Engine/EngineTypes.h"
|
||||
#include "Engine/HitResult.h"
|
||||
#include "DirectiveUtilTask_AsyncTrace.generated.h"
|
||||
@@ -28,7 +28,7 @@ enum class EDirectiveUtilTraceShape : uint8
|
||||
* The trace cannot be cancelled.
|
||||
*/
|
||||
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Trace"))
|
||||
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_AsyncTrace : public UBlueprintAsyncActionBase
|
||||
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_AsyncTrace : public UDirectiveUtilAsyncActionBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintAsyncActionBase.h"
|
||||
#include "Tasks/DirectiveUtilAsyncActionBase.h"
|
||||
#include "Engine/TimerHandle.h"
|
||||
#include "DirectiveUtilTask_Delay.generated.h"
|
||||
|
||||
@@ -11,10 +11,10 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnDelayCompleted);
|
||||
|
||||
/**
|
||||
* DirectiveUtilTask_Delay
|
||||
* A cancellable delay that can be ended early by calling EndTask.
|
||||
* A 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()
|
||||
|
||||
@@ -23,10 +23,10 @@ public:
|
||||
/**
|
||||
* Starts a cancellable delay.
|
||||
* 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 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(
|
||||
BlueprintCallable,
|
||||
@@ -38,14 +38,13 @@ public:
|
||||
))
|
||||
static UDirectiveUtilTask_Delay* CancellableDelay(UObject* WorldContextObject, float Duration);
|
||||
|
||||
/**
|
||||
* Ends the delay early.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|FlowControl")
|
||||
UFUNCTION(BlueprintCallable, meta=(DeprecatedFunction, DeprecationMessage="Use Cancel instead."), Category = "Directive Utilities|FlowControl")
|
||||
void EndTask();
|
||||
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)
|
||||
FOnDelayCompleted Completed;
|
||||
|
||||
@@ -55,6 +54,7 @@ protected:
|
||||
TObjectPtr<UObject> WorldContextObject;
|
||||
|
||||
float Duration = 0.0f;
|
||||
bool bFinished = false;
|
||||
FTimerHandle TimerHandle;
|
||||
|
||||
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
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintAsyncActionBase.h"
|
||||
#include "Tasks/DirectiveUtilAsyncActionBase.h"
|
||||
#include "GameFramework/Controller.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.
|
||||
*/
|
||||
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()
|
||||
|
||||
@@ -27,15 +27,15 @@ public:
|
||||
* bSuccess is true only when the pawn ends within AcceptanceRadius of Destination; the task also ends
|
||||
* (with the same distance test) when path-following stops for any reason.
|
||||
*
|
||||
* If the controller or pawn is destroyed while moving, the task will automatically end.
|
||||
* If 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.
|
||||
*
|
||||
* @param WorldContextObject The world context object.
|
||||
* @param Controller The controller to move.
|
||||
* @param Destination The vector location to move to.
|
||||
* @param AcceptanceRadius The radius around the destination location that is considered acceptable. Be sure to set this to a reasonable value as the controller may never reach the exact destination.
|
||||
* @param 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 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.
|
||||
*/
|
||||
UFUNCTION(
|
||||
@@ -86,6 +86,8 @@ protected:
|
||||
|
||||
FTimerHandle StuckTimerHandle;
|
||||
|
||||
TWeakObjectPtr<UWorld> TimerWorld;
|
||||
|
||||
bool bHasCompleted = false;
|
||||
|
||||
void CheckMoveToLocation();
|
||||
@@ -100,7 +102,7 @@ protected:
|
||||
* Asynchronously moves an actor to another 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()
|
||||
|
||||
@@ -113,15 +115,15 @@ public:
|
||||
* (with the same distance test) when path-following stops for any reason. The goal's location is re-read
|
||||
* every poll, so a moving goal is tracked.
|
||||
*
|
||||
* If the controller, pawn, or goal actor is destroyed while moving, the task will automatically end.
|
||||
* If 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.
|
||||
*
|
||||
* @param WorldContextObject The world context object.
|
||||
* @param Controller The controller to move.
|
||||
* @param Goal The actor to move to.
|
||||
* @param AcceptanceRadius The radius around the goal actor that is considered acceptable. Be sure to set this to a reasonable value as the controller may never reach the goal's exact location.
|
||||
* @param 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 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(
|
||||
BlueprintCallable,
|
||||
@@ -171,6 +173,8 @@ protected:
|
||||
|
||||
FTimerHandle StuckTimerHandle;
|
||||
|
||||
TWeakObjectPtr<UWorld> TimerWorld;
|
||||
|
||||
bool bHasCompleted = false;
|
||||
|
||||
void CheckMoveToActor();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#pragma once
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "DirectiveUtilInputTypes.generated.h"
|
||||
|
||||
@@ -5,8 +5,37 @@
|
||||
#include "CoreMinimal.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)
|
||||
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."),
|
||||
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."),
|
||||
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 "DirectiveUtilTypes.generated.h"
|
||||
@@ -12,3 +14,39 @@ enum class EDirectiveUtilSuccessStatus : uint8
|
||||
Success,
|
||||
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",
|
||||
"AutomationTest",
|
||||
"EnhancedInput",
|
||||
"GameplayTags"
|
||||
"GameplayTags",
|
||||
"Projects"
|
||||
}
|
||||
);
|
||||
|
||||
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("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 "GameFramework/DefaultPawn.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "TimerManager.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
|
||||
namespace DirectiveUtilAsyncTraceTestHelpers
|
||||
{
|
||||
/** Creates a transient game world with a physics scene, initialized for play so traces resolve. */
|
||||
UWorld* CreateTraceWorld()
|
||||
{
|
||||
UWorld* World = UWorld::CreateWorld(EWorldType::Game, false);
|
||||
@@ -33,7 +31,6 @@ namespace DirectiveUtilAsyncTraceTestHelpers
|
||||
return World;
|
||||
}
|
||||
|
||||
/** Spawns a blocking cube actor at the origin so traces have something to hit. */
|
||||
AStaticMeshActor* SpawnBlockingCube(UWorld* World, UStaticMesh* CubeMesh)
|
||||
{
|
||||
AStaticMeshActor* Cube = World->SpawnActor<AStaticMeshActor>(FVector::ZeroVector, FRotator::ZeroRotator);
|
||||
@@ -42,15 +39,36 @@ namespace DirectiveUtilAsyncTraceTestHelpers
|
||||
Component->SetStaticMesh(CubeMesh);
|
||||
Component->SetCollisionProfileName(TEXT("BlockAll"));
|
||||
Component->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
|
||||
Component->UpdateCollisionProfile();
|
||||
return Cube;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Latent command that ticks a trace world each frame until every listener has reported completion
|
||||
* (or the frame budget runs out), asserts the expected outcome, and tears the world down.
|
||||
*/
|
||||
bool UDirectiveUtilTestMoveToLocationTask::HasRegisteredTimers() const
|
||||
{
|
||||
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
|
||||
{
|
||||
public:
|
||||
@@ -111,18 +129,12 @@ private:
|
||||
int32 FramesRemaining;
|
||||
};
|
||||
|
||||
/**
|
||||
* DirectiveUtilTask_AsyncTrace: verifies the null-world guard broadcasts an empty result, and that each trace
|
||||
* shape (line, sphere, box, capsule) resolves against a blocking body and reports a hit.
|
||||
*/
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilAsyncTraceTest, "DirectiveUtilities.AsyncTaskTraceTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilAsyncTraceTest, "DirectiveUtilities.AsyncTaskTraceTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilAsyncTraceTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// The null-world activation intentionally logs a warning.
|
||||
AddExpectedMessagePlain(TEXT("Async Trace failed to activate. World is null."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
|
||||
// Null world guard: activating with a null context broadcasts an empty result and does not crash.
|
||||
{
|
||||
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
|
||||
Listener->AddToRoot();
|
||||
@@ -142,8 +154,8 @@ bool FDirectiveUtilAsyncTraceTest::RunTest(const FString& Parameters)
|
||||
UStaticMesh* CubeMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
|
||||
if (!CubeMesh)
|
||||
{
|
||||
AddInfo(TEXT("Engine cube mesh unavailable; skipping async trace hit scenarios."));
|
||||
return true;
|
||||
AddError(TEXT("Engine cube mesh unavailable for async trace hit scenarios."));
|
||||
return false;
|
||||
}
|
||||
|
||||
UWorld* World = DirectiveUtilAsyncTraceTestHelpers::CreateTraceWorld();
|
||||
@@ -154,10 +166,9 @@ bool FDirectiveUtilAsyncTraceTest::RunTest(const FString& Parameters)
|
||||
}
|
||||
DirectiveUtilAsyncTraceTestHelpers::SpawnBlockingCube(World, CubeMesh);
|
||||
|
||||
// Trace straight down through the cube at the origin so every shape intersects it.
|
||||
const FVector Start(0.0f, 0.0f, 500.0f);
|
||||
const FVector End(0.0f, 0.0f, -500.0f);
|
||||
const ETraceTypeQuery Channel = ETraceTypeQuery::TraceTypeQuery1; // Visibility, which BlockAll blocks.
|
||||
const ETraceTypeQuery VisibilityChannel = ETraceTypeQuery::TraceTypeQuery1;
|
||||
|
||||
auto MakeListener = [](UDirectiveUtilTask_AsyncTrace* Task) -> UDirectiveUtilDelegateListener*
|
||||
{
|
||||
@@ -170,82 +181,23 @@ bool FDirectiveUtilAsyncTraceTest::RunTest(const FString& Parameters)
|
||||
};
|
||||
|
||||
TArray<UDirectiveUtilDelegateListener*> Listeners;
|
||||
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncLineTraceByChannel(World, Start, End, Channel, false)));
|
||||
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncSphereTraceByChannel(World, Start, End, 25.0f, Channel, false)));
|
||||
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncBoxTraceByChannel(World, Start, End, FVector(25.0f), FRotator::ZeroRotator, Channel, false)));
|
||||
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncCapsuleTraceByChannel(World, Start, End, 25.0f, 50.0f, Channel, false)));
|
||||
Listeners.Add(MakeListener(UDirectiveUtilTask_AsyncTrace::AsyncLineTraceByChannel(World, Start, End, VisibilityChannel, 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, VisibilityChannel, 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));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Latent command that ticks a move-to-location world each frame until the listener reports
|
||||
* completion (or the frame budget runs out), asserts a single failed completion, and tears the
|
||||
* world down.
|
||||
*/
|
||||
class FDirectiveUtilTickMoveToLocationWorld : public IAutomationLatentCommand
|
||||
{
|
||||
public:
|
||||
FDirectiveUtilTickMoveToLocationWorld(FAutomationTestBase* InTest, UWorld* InWorld, UDirectiveUtilDelegateListener* InListener, int32 InFrames)
|
||||
: Test(InTest)
|
||||
, World(InWorld)
|
||||
, Listener(InListener)
|
||||
, FramesRemaining(InFrames)
|
||||
{
|
||||
}
|
||||
|
||||
virtual bool Update() override
|
||||
{
|
||||
if (UWorld* TickWorld = World.Get())
|
||||
{
|
||||
TickWorld->Tick(LEVELTICK_All, 0.05f);
|
||||
}
|
||||
|
||||
if (Listener && !Listener->bCompleted && --FramesRemaining > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Listener)
|
||||
{
|
||||
Test->TestTrue(TEXT("Move without navigation broadcasts Completed"), Listener->bCompleted);
|
||||
Test->TestFalse(TEXT("Move without navigation reports failure"), Listener->bLastSuccess);
|
||||
Test->TestEqual(TEXT("Move without navigation completes exactly once"), Listener->CompletedCount, 1);
|
||||
Listener->Keepalive = nullptr;
|
||||
Listener->RemoveFromRoot();
|
||||
}
|
||||
|
||||
if (UWorld* TearDownWorld = World.Get())
|
||||
{
|
||||
GEngine->DestroyWorldContext(TearDownWorld);
|
||||
TearDownWorld->DestroyWorld(false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
FAutomationTestBase* Test;
|
||||
TWeakObjectPtr<UWorld> World;
|
||||
UDirectiveUtilDelegateListener* Listener;
|
||||
int32 FramesRemaining;
|
||||
};
|
||||
|
||||
/**
|
||||
* DirectiveUtilTask_MoveToLocation: verifies the guard paths (null controller, controller without a pawn) and
|
||||
* EndTask all broadcast Completed(false) without crashing, that a second EndTask does not broadcast
|
||||
* again, and that a move with no navigation data terminates with failure. The successful navigation
|
||||
* path requires a built navigation mesh and is exercised in a project-level test rather than here.
|
||||
*/
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMoveToLocationTest, "DirectiveUtilities.AsyncTaskMoveToLocationTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMoveToLocationTest, "DirectiveUtilities.AsyncTaskMoveToLocationTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
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);
|
||||
|
||||
// Null controller guard: activating broadcasts Completed(false).
|
||||
{
|
||||
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
|
||||
Listener->AddToRoot();
|
||||
@@ -262,16 +214,15 @@ bool FDirectiveUtilMoveToLocationTest::RunTest(const FString& Parameters)
|
||||
Listener->RemoveFromRoot();
|
||||
}
|
||||
|
||||
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
|
||||
UWorld* World = UWorld::CreateWorld(EWorldType::Game, false);
|
||||
if (!World)
|
||||
{
|
||||
AddError(TEXT("Failed to create a transient world for the move-to-location test."));
|
||||
return false;
|
||||
}
|
||||
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor);
|
||||
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Game);
|
||||
WorldContext.SetCurrentWorld(World);
|
||||
|
||||
// Controller-without-pawn guard: activating broadcasts Completed(false).
|
||||
{
|
||||
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
||||
|
||||
@@ -290,7 +241,6 @@ bool FDirectiveUtilMoveToLocationTest::RunTest(const FString& Parameters)
|
||||
Listener->RemoveFromRoot();
|
||||
}
|
||||
|
||||
// EndTask broadcasts Completed(false) and clears timers without crashing.
|
||||
{
|
||||
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
||||
|
||||
@@ -309,7 +259,6 @@ bool FDirectiveUtilMoveToLocationTest::RunTest(const FString& Parameters)
|
||||
Listener->RemoveFromRoot();
|
||||
}
|
||||
|
||||
// Double completion guard: a second EndTask does not broadcast Completed again.
|
||||
{
|
||||
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
||||
|
||||
@@ -328,31 +277,44 @@ bool FDirectiveUtilMoveToLocationTest::RunTest(const FString& Parameters)
|
||||
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);
|
||||
World->DestroyWorld(false);
|
||||
|
||||
// No-navigation failure: without a navmesh the idle path-following check terminates the task
|
||||
// with failure instead of polling forever.
|
||||
{
|
||||
// SimpleMoveToLocation may warn when the world has no navigation system.
|
||||
AddExpectedMessagePlain(TEXT("SimpleMoveToActor called for NavSys:"), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
AddExpectedMessagePlain(TEXT("SimpleMove failed for"), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
|
||||
UWorld* MoveWorld = DirectiveUtilAsyncTraceTestHelpers::CreateTraceWorld();
|
||||
if (!MoveWorld)
|
||||
{
|
||||
AddInfo(TEXT("Failed to create a transient game world; skipping the no-navigation move scenario."));
|
||||
return true;
|
||||
AddError(TEXT("Failed to create a transient game world for the no-navigation move scenario."));
|
||||
return false;
|
||||
}
|
||||
|
||||
APlayerController* Controller = MoveWorld->SpawnActor<APlayerController>();
|
||||
ADefaultPawn* Pawn = MoveWorld->SpawnActor<ADefaultPawn>(FVector::ZeroVector, FRotator::ZeroRotator);
|
||||
if (!Controller || !Pawn)
|
||||
{
|
||||
AddInfo(TEXT("Failed to spawn a controller or pawn; skipping the no-navigation move scenario."));
|
||||
AddError(TEXT("Failed to spawn a controller or pawn for the no-navigation move scenario."));
|
||||
GEngine->DestroyWorldContext(MoveWorld);
|
||||
MoveWorld->DestroyWorld(false);
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
Controller->SetPawn(Pawn);
|
||||
|
||||
@@ -364,26 +326,26 @@ bool FDirectiveUtilMoveToLocationTest::RunTest(const FString& Parameters)
|
||||
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilMoveToActorTest, "DirectiveUtilities.AsyncTaskMoveToActorTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
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);
|
||||
|
||||
// Null controller guard: activating broadcasts Completed(false).
|
||||
{
|
||||
UDirectiveUtilDelegateListener* Listener = NewObject<UDirectiveUtilDelegateListener>();
|
||||
Listener->AddToRoot();
|
||||
@@ -400,16 +362,15 @@ bool FDirectiveUtilMoveToActorTest::RunTest(const FString& Parameters)
|
||||
Listener->RemoveFromRoot();
|
||||
}
|
||||
|
||||
UWorld* World = UWorld::CreateWorld(EWorldType::Editor, false);
|
||||
UWorld* World = UWorld::CreateWorld(EWorldType::Game, false);
|
||||
if (!World)
|
||||
{
|
||||
AddError(TEXT("Failed to create a transient world for the move-to-actor test."));
|
||||
return false;
|
||||
}
|
||||
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Editor);
|
||||
FWorldContext& WorldContext = GEngine->CreateNewWorldContext(EWorldType::Game);
|
||||
WorldContext.SetCurrentWorld(World);
|
||||
|
||||
// Null goal guard: a controller with a pawn but no goal broadcasts Completed(false).
|
||||
{
|
||||
APlayerController* Controller = World->SpawnActor<APlayerController>();
|
||||
ADefaultPawn* Pawn = World->SpawnActor<ADefaultPawn>(FVector::ZeroVector, FRotator::ZeroRotator);
|
||||
@@ -433,11 +394,10 @@ bool FDirectiveUtilMoveToActorTest::RunTest(const FString& Parameters)
|
||||
}
|
||||
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>();
|
||||
|
||||
@@ -456,21 +416,35 @@ bool FDirectiveUtilMoveToActorTest::RunTest(const FString& Parameters)
|
||||
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);
|
||||
World->DestroyWorld(false);
|
||||
|
||||
// No-navigation failure: without a navmesh the idle path-following check terminates the task
|
||||
// with failure instead of polling forever.
|
||||
{
|
||||
// SimpleMoveToActor may warn when the world has no navigation system.
|
||||
AddExpectedMessagePlain(TEXT("SimpleMoveToActor called for NavSys:"), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
AddExpectedMessagePlain(TEXT("SimpleMove failed for"), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
|
||||
UWorld* MoveWorld = DirectiveUtilAsyncTraceTestHelpers::CreateTraceWorld();
|
||||
if (!MoveWorld)
|
||||
{
|
||||
AddInfo(TEXT("Failed to create a transient game world; skipping the no-navigation move scenario."));
|
||||
return true;
|
||||
AddError(TEXT("Failed to create a transient game world for the no-navigation move scenario."));
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
MoveWorld->DestroyWorld(false);
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
Controller->SetPawn(Pawn);
|
||||
|
||||
@@ -493,10 +467,14 @@ bool FDirectiveUtilMoveToActorTest::RunTest(const FString& Parameters)
|
||||
Task->Completed.AddDynamic(Listener, &UDirectiveUtilDelegateListener::OnBoolCompleted);
|
||||
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;
|
||||
}
|
||||
|
||||
#endif // WITH_EDITOR
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "UObject/Class.h"
|
||||
#include "UObject/Package.h"
|
||||
#include "UObject/UnrealType.h"
|
||||
#include "UObject/UObjectIterator.h"
|
||||
|
||||
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)
|
||||
{
|
||||
const FString ExpectedCategoryRoot = TEXT("Directive Utilities");
|
||||
const FString RuntimeScriptPackage = TEXT("/Script/DirectiveUtilitiesRuntime");
|
||||
const TSet<FString> PluginScriptPackages = {
|
||||
TEXT("/Script/DirectiveUtilitiesRuntime"),
|
||||
RuntimeScriptPackage,
|
||||
TEXT("/Script/DirectiveUtilitiesEditor")
|
||||
};
|
||||
|
||||
@@ -32,6 +36,15 @@ bool FDirectiveUtilBlueprintCategoryTest::RunTest(const FString& Parameters)
|
||||
}
|
||||
|
||||
++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"));
|
||||
FString CategoryRoot = Category;
|
||||
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
|
||||
|
||||
#include "Subsystems/DirectiveUtilEditorActorSubsystem.h"
|
||||
@@ -5,17 +7,23 @@
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "Engine/StaticMesh.h"
|
||||
#include "Engine/StaticMeshActor.h"
|
||||
#include "Engine/Texture2D.h"
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
#include "Components/BoxComponent.h"
|
||||
#include "Components/CapsuleComponent.h"
|
||||
#include "Materials/Material.h"
|
||||
#include "Materials/MaterialExpressionTextureSample.h"
|
||||
#include "Materials/MaterialInterface.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "UObject/Package.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemTest, "DirectiveUtilities.EditorActorSubsystemTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilEditorActorSubsystemTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
UDirectiveUtilEditorActorSubsystem::FocusActorsInViewport({nullptr});
|
||||
|
||||
// IsActorWithinBoxBounds should not crash and should return false with null Actor
|
||||
TestFalse("IsActorWithinBoxBounds should return false with null Actor",
|
||||
UDirectiveUtilEditorActorSubsystem::IsActorWithinBoxBounds(nullptr, nullptr));
|
||||
@@ -60,8 +68,8 @@ bool FDirectiveUtilEditorActorSubsystemFilterTest::RunTest(const FString& Parame
|
||||
UStaticMesh* Sphere = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Sphere.Sphere"));
|
||||
if (!Cube || !Sphere)
|
||||
{
|
||||
AddInfo(TEXT("Engine basic shapes unavailable; skipping include/exclude behaviour test."));
|
||||
return true;
|
||||
AddError(TEXT("Engine basic shapes unavailable for the include/exclude behaviour test."));
|
||||
return 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"));
|
||||
if (!Cube || !Sphere || !MatA || !MatB)
|
||||
{
|
||||
AddInfo(TEXT("Engine basic shapes/materials unavailable; skipping filter coverage test."));
|
||||
return true;
|
||||
AddError(TEXT("Engine basic shapes/materials unavailable for the filter coverage test."));
|
||||
return 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));
|
||||
}
|
||||
|
||||
// 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.
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
World->DestroyWorld(false);
|
||||
|
||||
@@ -327,10 +421,12 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilEditorActorSubsystemQueryAlignmen
|
||||
bool FDirectiveUtilEditorActorSubsystemQueryAlignmentTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
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."));
|
||||
return true;
|
||||
AddError(TEXT("Engine basic shapes or materials unavailable for the query alignment test."));
|
||||
return false;
|
||||
}
|
||||
|
||||
// The GetActorsBy* queries read from the editor world rather than a passed array.
|
||||
@@ -345,8 +441,8 @@ bool FDirectiveUtilEditorActorSubsystemQueryAlignmentTest::RunTest(const FString
|
||||
}
|
||||
if (!EditorWorld)
|
||||
{
|
||||
AddInfo(TEXT("No editor world available; skipping query alignment test."));
|
||||
return true;
|
||||
AddError(TEXT("No editor world available for the query alignment test."));
|
||||
return false;
|
||||
}
|
||||
|
||||
AActor* MeshActor = EditorWorld->SpawnActor<AActor>();
|
||||
@@ -358,6 +454,7 @@ bool FDirectiveUtilEditorActorSubsystemQueryAlignmentTest::RunTest(const FString
|
||||
MeshComponent->SetMobility(EComponentMobility::Movable);
|
||||
MeshComponent->SetupAttachment(Root);
|
||||
MeshComponent->SetStaticMesh(Cube);
|
||||
MeshComponent->SetMaterial(0, BasicMaterial);
|
||||
MeshComponent->RegisterComponent();
|
||||
MeshActor->AddInstanceComponent(MeshComponent);
|
||||
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.
|
||||
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.
|
||||
TArray<AActor*> InBox;
|
||||
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);
|
||||
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.
|
||||
const TArray<AActor*> Source = { MeshActor };
|
||||
TArray<AActor*> MovableActors;
|
||||
@@ -392,6 +568,47 @@ bool FDirectiveUtilEditorActorSubsystemQueryAlignmentTest::RunTest(const FString
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
#include "AssetRegistry/AssetData.h"
|
||||
#include "AssetRegistry/AssetRegistryModule.h"
|
||||
#include "Editor.h"
|
||||
#include "Libraries/DirectiveUtilEditorAssetLibrary.h"
|
||||
#include "Engine/World.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)
|
||||
|
||||
@@ -45,6 +55,78 @@ bool FDirectiveUtilEditorAssetLibraryTest::RunTest(const FString& Parameters)
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
#include "Tests/AutomationCommon.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
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)
|
||||
{
|
||||
// Preserve the user/CI clipboard so this test doesn't destroy it, and restore it before returning.
|
||||
const FString OriginalClipboard = UDirectiveUtilFunctionLibrary::GetStringFromClipboard();
|
||||
|
||||
const FText TestText = FText::FromString(TEXT("Hello, Clipboard!"));
|
||||
UDirectiveUtilFunctionLibrary::CopyTextToClipboard(TestText);
|
||||
const FText ClipboardText = UDirectiveUtilFunctionLibrary::GetTextFromClipboard();
|
||||
TestEqual("GetTextFromClipboard should return the copied text", ClipboardText.ToString(), TestText.ToString());
|
||||
|
||||
ADD_LATENT_AUTOMATION_COMMAND(FEngineWaitLatentCommand(1.0f));
|
||||
|
||||
const FString TestString = TEXT("Hello, Clipboard!");
|
||||
UDirectiveUtilFunctionLibrary::CopyStringToClipboard(TestString);
|
||||
const FString ClipboardString = UDirectiveUtilFunctionLibrary::GetStringFromClipboard();
|
||||
TestEqual("GetStringFromClipboard should return the copied string", ClipboardString, TestString);
|
||||
|
||||
ADD_LATENT_AUTOMATION_COMMAND(FEngineWaitLatentCommand(1.0f));
|
||||
|
||||
UDirectiveUtilFunctionLibrary::ClearClipboard();
|
||||
const FString ClearedClipboardString = UDirectiveUtilFunctionLibrary::GetStringFromClipboard();
|
||||
TestEqual("GetStringFromClipboard should return an empty string after clearing the clipboard", ClearedClipboardString, TEXT(""));
|
||||
ADD_LATENT_AUTOMATION_COMMAND(DirectiveUtilFunctionLibraryTest::FClipboardRoundTripCommand(this, OriginalClipboard));
|
||||
|
||||
const FString ProjectVersion = UDirectiveUtilFunctionLibrary::GetProjectVersion();
|
||||
TestNotEqual("GetProjectVersion should return a non-empty string", ProjectVersion, FString(""));
|
||||
|
||||
TestTrue("IsRunningInEditor should return true in editor context",
|
||||
UDirectiveUtilFunctionLibrary::IsRunningInEditor());
|
||||
#if WITH_EDITOR
|
||||
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;
|
||||
UDirectiveUtilFunctionLibrary::GetChildClasses(UBlueprintFunctionLibrary::StaticClass(), true, RecursiveDerived);
|
||||
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",
|
||||
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;
|
||||
UDirectiveUtilFunctionLibrary::GetChildClasses(UBlueprintFunctionLibrary::StaticClass(), false, NonRecursiveDerived);
|
||||
TestTrue("GetChildClasses non-recursive count should not exceed recursive count",
|
||||
NonRecursiveDerived.Num() <= RecursiveDerived.Num());
|
||||
|
||||
// Leaf class with no subclasses: an empty input array should remain empty.
|
||||
TArray<UClass*> LeafDerived;
|
||||
UDirectiveUtilFunctionLibrary::GetChildClasses(UDirectiveUtilFunctionLibrary::StaticClass(), true, LeafDerived);
|
||||
TestEqual("GetChildClasses should return an empty list for a class with no subclasses",
|
||||
LeafDerived.Num(), 0);
|
||||
|
||||
// Null base class: should not crash and should add nothing.
|
||||
TArray<UClass*> NullBaseDerived;
|
||||
UDirectiveUtilFunctionLibrary::GetChildClasses(nullptr, true, NullBaseDerived);
|
||||
TestEqual("GetChildClasses should return an empty list for a null base class",
|
||||
@@ -87,11 +392,9 @@ bool FDirectiveUtilFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
TestFalse("GetCommandLineOption should return false for an empty key",
|
||||
UDirectiveUtilFunctionLibrary::GetCommandLineOption(CommandLine, TEXT(""), Value));
|
||||
|
||||
// Smoke test through the process-command-line path.
|
||||
TestFalse("HasCommandLineSwitch should not find a switch that was never passed",
|
||||
UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(TEXT("DirectiveUtilitiesDefinitelyNotPassed")));
|
||||
}
|
||||
|
||||
UDirectiveUtilFunctionLibrary::CopyStringToClipboard(OriginalClipboard);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilGameplayTagFunctionLibrary.h"
|
||||
#include "GameplayTagsManager.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
#include "Misc/FileHelper.h"
|
||||
#include "Misc/Paths.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilGameplayTagFunctionLibraryTest, "DirectiveUtilities.GameplayTagFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilGameplayTagFunctionLibraryTest, "DirectiveUtilities.GameplayTagFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "Types/DirectiveUtilInputTypes.h"
|
||||
#include "Types/DirectiveUtilTypes.h"
|
||||
#include "InputMappingContext.h"
|
||||
#include "EnhancedPlayerInput.h"
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "Engine/LocalPlayer.h"
|
||||
@@ -12,34 +13,24 @@
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
#if WITH_EDITOR
|
||||
|
||||
/**
|
||||
* Exercises the Enhanced Input library's active paths against a real EnhancedInput subsystem,
|
||||
* which requires a live local player. A standalone game instance + local player is built for this;
|
||||
* if that cannot be created in the headless harness the test skips its assertions rather than failing.
|
||||
*/
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilInputActivePathTest, "DirectiveUtilities.InputActivePathTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilInputActivePathTest, "DirectiveUtilities.InputActivePathTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilInputActivePathTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// These warnings only fire on the graceful-skip paths; allow (but never require) them.
|
||||
AddExpectedMessagePlain(TEXT("LocalPlayer not found. Cannot set input mapping contexts."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
AddExpectedMessagePlain(TEXT("EnhancedInput subsystem not found."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
|
||||
UGameInstance* GameInstance = NewObject<UGameInstance>(GEngine);
|
||||
if (!GameInstance)
|
||||
{
|
||||
AddInfo(TEXT("Could not create a game instance; skipping Enhanced Input active-path assertions."));
|
||||
return true;
|
||||
AddError(TEXT("Could not create a game instance for Enhanced Input active-path assertions."));
|
||||
return false;
|
||||
}
|
||||
GameInstance->AddToRoot();
|
||||
GameInstance->InitializeStandalone();
|
||||
|
||||
UWorld* World = GameInstance->GetWorld();
|
||||
|
||||
// CreateLocalPlayer ensures when there is no game viewport client (headless), so build the local
|
||||
// player directly via AddLocalPlayer, which initializes the local-player subsystems without one.
|
||||
UClass* LocalPlayerClass = GEngine->LocalPlayerClass ? GEngine->LocalPlayerClass.Get() : ULocalPlayer::StaticClass();
|
||||
ULocalPlayer* LocalPlayer = NewObject<ULocalPlayer>(GEngine, LocalPlayerClass);
|
||||
if (World && LocalPlayer)
|
||||
@@ -49,33 +40,40 @@ bool FDirectiveUtilInputActivePathTest::RunTest(const FString& Parameters)
|
||||
|
||||
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->RemoveFromRoot();
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
APlayerController* PlayerController = World->SpawnActor<APlayerController>();
|
||||
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->RemoveFromRoot();
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
PlayerController->SetPlayer(LocalPlayer);
|
||||
PlayerController->InitInputSystem();
|
||||
PlayerController->PlayerInput = NewObject<UEnhancedPlayerInput>(PlayerController);
|
||||
|
||||
UEnhancedInputLocalPlayerSubsystem* Subsystem = UDirectiveUtilInputFunctionLibrary::GetEnhancedInputSubsystem(PlayerController);
|
||||
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->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);
|
||||
auto ApplyPendingMappings = [Subsystem]
|
||||
{
|
||||
FModifyContextOptions Options;
|
||||
Options.bForceImmediately = true;
|
||||
Subsystem->RequestRebuildControlMappings(Options);
|
||||
};
|
||||
|
||||
UInputMappingContext* ContextA = 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> SoftB(ContextB);
|
||||
|
||||
// Add context A.
|
||||
FDirectiveUtilEnhancedInputContextData DataA;
|
||||
DataA.InputContext = SoftA;
|
||||
DataA.Priority = 0;
|
||||
@@ -93,38 +90,47 @@ bool FDirectiveUtilInputActivePathTest::RunTest(const FString& Parameters)
|
||||
TestEqual("AddInputMappingContexts succeeds for a live controller",
|
||||
UDirectiveUtilInputFunctionLibrary::AddInputMappingContexts(PlayerController, ToAdd, false),
|
||||
EDirectiveUtilSuccessStatus::Success);
|
||||
ApplyPendingMappings();
|
||||
TestTrue("Added context is reported active",
|
||||
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftA));
|
||||
|
||||
// Swap A -> B.
|
||||
TestEqual("SwapInputMappingContexts succeeds",
|
||||
UDirectiveUtilInputFunctionLibrary::SwapInputMappingContexts(PlayerController, SoftA, SoftB, 0, false),
|
||||
EDirectiveUtilSuccessStatus::Success);
|
||||
ApplyPendingMappings();
|
||||
TestFalse("Swapped-out context is inactive",
|
||||
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftA));
|
||||
TestTrue("Swapped-in context is active",
|
||||
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftB));
|
||||
|
||||
// Remove B.
|
||||
TArray<TSoftObjectPtr<UInputMappingContext>> ToRemove;
|
||||
ToRemove.Add(SoftB);
|
||||
TestEqual("RemoveInputMappingContexts succeeds",
|
||||
UDirectiveUtilInputFunctionLibrary::RemoveInputMappingContexts(PlayerController, ToRemove),
|
||||
EDirectiveUtilSuccessStatus::Success);
|
||||
ApplyPendingMappings();
|
||||
TestFalse("Removed context is inactive",
|
||||
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);
|
||||
TestEqual("ClearAllInputMappingContexts succeeds",
|
||||
UDirectiveUtilInputFunctionLibrary::ClearAllInputMappingContexts(PlayerController),
|
||||
EDirectiveUtilSuccessStatus::Success);
|
||||
ApplyPendingMappings();
|
||||
TestFalse("Context is inactive after clear-all",
|
||||
UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(PlayerController, SoftA));
|
||||
|
||||
// A clearing add whose every context fails to load must leave the existing mappings untouched.
|
||||
AddExpectedMessagePlain(TEXT("Input Mapping Contexts failed to load and were not added!"), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
AddExpectedMessagePlain(TEXT("input mapping contexts could not be loaded"), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
UDirectiveUtilInputFunctionLibrary::AddInputMappingContexts(PlayerController, ToAdd, false);
|
||||
ApplyPendingMappings();
|
||||
FDirectiveUtilEnhancedInputContextData UnresolvableData;
|
||||
UnresolvableData.Priority = 0;
|
||||
TArray<FDirectiveUtilEnhancedInputContextData> UnresolvableToAdd;
|
||||
@@ -142,5 +148,3 @@ bool FDirectiveUtilInputActivePathTest::RunTest(const FString& Parameters)
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif // WITH_EDITOR
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilInputFunctionLibrary.h"
|
||||
#include "Types/DirectiveUtilTypes.h"
|
||||
#include "InputMappingContext.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilInputFunctionLibraryTest, "DirectiveUtilities.InputFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilInputFunctionLibraryTest, "DirectiveUtilities.InputFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilInputFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
// The null-controller and invalid-context paths intentionally log warnings. Register them as
|
||||
// expected (plain match, negative count = consume if present, never required) so the run is clean.
|
||||
AddExpectedMessagePlain(TEXT("PlayerController is null. Cannot set input mapping contexts."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
AddExpectedMessagePlain(TEXT("Both the previous and new input mapping contexts must be valid."), ELogVerbosity::Warning, EAutomationExpectedMessageFlags::Contains, -1);
|
||||
|
||||
// AddInputMappingContexts should return Failure with null controller
|
||||
TArray<FDirectiveUtilEnhancedInputContextData> Contexts;
|
||||
@@ -39,8 +40,6 @@ bool FDirectiveUtilInputFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
UDirectiveUtilInputFunctionLibrary::RemoveInputMappingContexts(nullptr, EmptyRemoveContexts),
|
||||
EDirectiveUtilSuccessStatus::Failure);
|
||||
|
||||
// SwapInputMappingContexts loads both contexts before using the controller, so invalid
|
||||
// (unset) soft pointers must return Failure regardless of the (null) controller.
|
||||
const TSoftObjectPtr<UInputMappingContext> NullContext;
|
||||
TestEqual("SwapInputMappingContexts should fail when both contexts are invalid",
|
||||
UDirectiveUtilInputFunctionLibrary::SwapInputMappingContexts(nullptr, NullContext, NullContext, 0, false),
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilMapFunctionLibrary.h"
|
||||
#include "Tests/DirectiveUtilTestObject.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)
|
||||
{
|
||||
@@ -144,5 +146,11 @@ bool FDirectiveUtilMapFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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 "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)
|
||||
{
|
||||
@@ -11,8 +15,84 @@ bool FDirectiveUtilMathFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ForwardVector, FVector::RightVector), 90.0f, 0.01f));
|
||||
TestTrue("AngleBetweenVectors should return ~180 for opposite vectors",
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ForwardVector, -FVector::ForwardVector), 180.0f, 0.01f));
|
||||
TestTrue("AngleBetweenVectors should handle zero vector gracefully",
|
||||
FMath::IsFinite(UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(FVector::ZeroVector, FVector::ForwardVector)));
|
||||
TestEqual("AngleBetweenVectors should return 0 for a zero vector",
|
||||
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);
|
||||
@@ -75,15 +155,16 @@ bool FDirectiveUtilMathFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
const TArray<EDirectiveUtilEaseType> AllEaseTypes = {
|
||||
EDirectiveUtilEaseType::BackIn, EDirectiveUtilEaseType::BackOut, EDirectiveUtilEaseType::BackInOut,
|
||||
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)
|
||||
{
|
||||
const FString TypeName = FString::FromInt(static_cast<int32>(EaseType));
|
||||
TestTrue(FString::Printf(TEXT("EaseAlpha(0) should be ~0 for type %s"), *TypeName),
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::EaseAlpha(0.0f, EaseType), 0.0f, 1.e-3f));
|
||||
TestTrue(FString::Printf(TEXT("EaseAlpha(1) should be ~1 for type %s"), *TypeName),
|
||||
FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::EaseAlpha(1.0f, EaseType), 1.0f, 1.e-3f));
|
||||
TestEqual(FString::Printf(TEXT("EaseAlpha(0) should be exactly 0 for type %s"), *TypeName),
|
||||
UDirectiveUtilMathFunctionLibrary::EaseAlpha(0.0f, EaseType), 0.0f);
|
||||
TestEqual(FString::Printf(TEXT("EaseAlpha(1) should be exactly 1 for type %s"), *TypeName),
|
||||
UDirectiveUtilMathFunctionLibrary::EaseAlpha(1.0f, EaseType), 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),
|
||||
@@ -94,6 +175,22 @@ bool FDirectiveUtilMathFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
TestEqual("EaseAlpha should clamp alpha below 0",
|
||||
UDirectiveUtilMathFunctionLibrary::EaseAlpha(-1.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",
|
||||
UDirectiveUtilMathFunctionLibrary::EaseAlpha(2.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));
|
||||
TestEqual("Weighted random with all-zero weights returns 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)
|
||||
{
|
||||
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")));
|
||||
TestEqual("FormatDuration(-90) is \"-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\"",
|
||||
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);
|
||||
}
|
||||
|
||||
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 StreamB(12345);
|
||||
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 "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)
|
||||
{
|
||||
|
||||
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 "Tests/DirectiveUtilTestObject.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "GameFramework/SaveGame.h"
|
||||
#include "Misc/AutomationTest.h"
|
||||
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilSaveGameFunctionLibraryTest, "DirectiveUtilities.SaveGameFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
|
||||
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FDirectiveUtilSaveGameFunctionLibraryTest, "DirectiveUtilities.SaveGameFunctionLibraryTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter)
|
||||
|
||||
bool FDirectiveUtilSaveGameFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
{
|
||||
@@ -84,6 +86,11 @@ bool FDirectiveUtilSaveGameFunctionLibraryTest::RunTest(const FString& Parameter
|
||||
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(SlotB));
|
||||
TestNotNull("A renamed slot should still deserialize",
|
||||
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>();
|
||||
TestTrue("SaveGameToSlot should write the collision test slot",
|
||||
@@ -99,12 +106,18 @@ bool FDirectiveUtilSaveGameFunctionLibraryTest::RunTest(const FString& Parameter
|
||||
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(TEXT("../escape")));
|
||||
TestFalse("DeleteSaveSlot should reject an invalid slot name",
|
||||
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",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::RenameSaveSlot(TEXT("../escape"), SlotC));
|
||||
TestFalse("RenameSaveSlot should reject an invalid destination slot name",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::RenameSaveSlot(SlotC, TEXT("../escape")));
|
||||
TestTrue("A rejected rename should leave the source slot intact",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(SlotC));
|
||||
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",
|
||||
UDirectiveUtilSaveGameFunctionLibrary::DeleteSaveSlot(SlotB));
|
||||
@@ -115,6 +128,7 @@ bool FDirectiveUtilSaveGameFunctionLibraryTest::RunTest(const FString& Parameter
|
||||
// Clean up
|
||||
UGameplayStatics::DeleteGameInSlot(SlotA, 0);
|
||||
UGameplayStatics::DeleteGameInSlot(SlotB, 0);
|
||||
UGameplayStatics::DeleteGameInSlot(SlotB.ToLower(), 0);
|
||||
UGameplayStatics::DeleteGameInSlot(SlotC, 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,37 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilStringFunctionLibrary.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)
|
||||
{
|
||||
@@ -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 an empty string", UDirectiveUtilStringFunctionLibrary::IsValidFileName(TEXT("")));
|
||||
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"));
|
||||
TestTrue("SanitizeFileName should produce a name that IsValidFileName accepts",
|
||||
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("_"));
|
||||
TestTrue("SanitizeFileName should substitute the replacement character for stripped characters",
|
||||
Replaced.Contains(TEXT("_")));
|
||||
@@ -271,6 +316,43 @@ bool FDirectiveUtilStringFunctionLibraryTest::RunTest(const FString& Parameters)
|
||||
UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(TEXT(""), {TEXT("abc")}, Similarity), 0);
|
||||
TestTrue("FindBestStringMatch should report similarity 0 for an empty input against a non-empty candidate",
|
||||
FMath::IsNearlyEqual(Similarity, 0.0f, 1.e-4f));
|
||||
|
||||
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;
|
||||
|
||||
@@ -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 "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)
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user