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

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