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

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