Update directive utilities plugin

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

View File

@@ -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",
}
@@ -39,4 +40,4 @@ public class DirectiveUtilitiesRuntime : ModuleRules
}
);
}
}
}

View File

@@ -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);
DEFINE_LOG_CATEGORY(LogDirectiveUtilEditor);

View File

@@ -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())

View File

@@ -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)

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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();
});
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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();

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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;
}
};

View File

@@ -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);

View File

@@ -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);
/**

View File

@@ -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"))

View File

@@ -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);
/*~

View File

@@ -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);
};

View File

@@ -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.

View File

@@ -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.

View File

@@ -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);
};

View File

@@ -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;
};

View File

@@ -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()

View File

@@ -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()

View File

@@ -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();

View File

@@ -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();
};

View File

@@ -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();

View File

@@ -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"

View File

@@ -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."),
};

View File

@@ -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"
@@ -11,4 +13,40 @@ 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,
};