Update directive utilities plugin
This commit is contained in:
@@ -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);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,9 +2,92 @@
|
||||
|
||||
|
||||
#include "Libraries/DirectiveUtilFunctionLibrary.h"
|
||||
#include "Engine/World.h"
|
||||
#include "HAL/CriticalSection.h"
|
||||
#include "HAL/PlatformApplicationMisc.h"
|
||||
#include "HAL/PlatformTime.h"
|
||||
#include "Misc/App.h"
|
||||
#include "Misc/CommandLine.h"
|
||||
#include "Misc/ConfigCacheIni.h"
|
||||
#include "Misc/ScopeLock.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
struct FDirectiveUtilStopwatchState
|
||||
{
|
||||
FCriticalSection Lock;
|
||||
TMap<FName, double> StartTimesByKey;
|
||||
};
|
||||
|
||||
FDirectiveUtilStopwatchState StopwatchState;
|
||||
|
||||
EDirectiveUtilWorldType ToDirectiveWorldType(const EWorldType::Type WorldType)
|
||||
{
|
||||
switch (WorldType)
|
||||
{
|
||||
case EWorldType::None:
|
||||
return EDirectiveUtilWorldType::None;
|
||||
case EWorldType::Game:
|
||||
return EDirectiveUtilWorldType::Game;
|
||||
case EWorldType::Editor:
|
||||
return EDirectiveUtilWorldType::Editor;
|
||||
case EWorldType::PIE:
|
||||
return EDirectiveUtilWorldType::PlayInEditor;
|
||||
case EWorldType::EditorPreview:
|
||||
return EDirectiveUtilWorldType::EditorPreview;
|
||||
case EWorldType::GamePreview:
|
||||
return EDirectiveUtilWorldType::GamePreview;
|
||||
case EWorldType::GameRPC:
|
||||
return EDirectiveUtilWorldType::GameRPC;
|
||||
case EWorldType::Inactive:
|
||||
return EDirectiveUtilWorldType::Inactive;
|
||||
}
|
||||
|
||||
return EDirectiveUtilWorldType::Unknown;
|
||||
}
|
||||
|
||||
EDirectiveUtilBuildConfiguration ToDirectiveBuildConfiguration(const EBuildConfiguration BuildConfiguration)
|
||||
{
|
||||
switch (BuildConfiguration)
|
||||
{
|
||||
case EBuildConfiguration::Unknown:
|
||||
return EDirectiveUtilBuildConfiguration::Unknown;
|
||||
case EBuildConfiguration::Debug:
|
||||
return EDirectiveUtilBuildConfiguration::Debug;
|
||||
case EBuildConfiguration::DebugGame:
|
||||
return EDirectiveUtilBuildConfiguration::DebugGame;
|
||||
case EBuildConfiguration::Development:
|
||||
return EDirectiveUtilBuildConfiguration::Development;
|
||||
case EBuildConfiguration::Shipping:
|
||||
return EDirectiveUtilBuildConfiguration::Shipping;
|
||||
case EBuildConfiguration::Test:
|
||||
return EDirectiveUtilBuildConfiguration::Test;
|
||||
}
|
||||
|
||||
return EDirectiveUtilBuildConfiguration::Unknown;
|
||||
}
|
||||
|
||||
EDirectiveUtilBuildTargetType ToDirectiveBuildTargetType(const EBuildTargetType BuildTargetType)
|
||||
{
|
||||
switch (BuildTargetType)
|
||||
{
|
||||
case EBuildTargetType::Unknown:
|
||||
return EDirectiveUtilBuildTargetType::Unknown;
|
||||
case EBuildTargetType::Game:
|
||||
return EDirectiveUtilBuildTargetType::Game;
|
||||
case EBuildTargetType::Server:
|
||||
return EDirectiveUtilBuildTargetType::Server;
|
||||
case EBuildTargetType::Client:
|
||||
return EDirectiveUtilBuildTargetType::Client;
|
||||
case EBuildTargetType::Editor:
|
||||
return EDirectiveUtilBuildTargetType::Editor;
|
||||
case EBuildTargetType::Program:
|
||||
return EDirectiveUtilBuildTargetType::Program;
|
||||
}
|
||||
|
||||
return EDirectiveUtilBuildTargetType::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
void UDirectiveUtilFunctionLibrary::GetChildClasses(const UClass* BaseClass, const bool bRecursive, TArray<UClass*>& DerivedClasses)
|
||||
{
|
||||
@@ -57,6 +140,22 @@ bool UDirectiveUtilFunctionLibrary::IsRunningInEditor()
|
||||
return GIsEditor;
|
||||
}
|
||||
|
||||
EDirectiveUtilWorldType UDirectiveUtilFunctionLibrary::GetWorldType(const UObject* WorldContextObject)
|
||||
{
|
||||
const UWorld* World = WorldContextObject ? WorldContextObject->GetWorld() : nullptr;
|
||||
return World ? ToDirectiveWorldType(World->WorldType) : EDirectiveUtilWorldType::Unknown;
|
||||
}
|
||||
|
||||
EDirectiveUtilBuildConfiguration UDirectiveUtilFunctionLibrary::GetBuildConfigurationType()
|
||||
{
|
||||
return ToDirectiveBuildConfiguration(FApp::GetBuildConfiguration());
|
||||
}
|
||||
|
||||
EDirectiveUtilBuildTargetType UDirectiveUtilFunctionLibrary::GetBuildTargetType()
|
||||
{
|
||||
return ToDirectiveBuildTargetType(FApp::GetBuildTargetType());
|
||||
}
|
||||
|
||||
bool UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(const FString& Switch)
|
||||
{
|
||||
return HasCommandLineSwitch(FCommandLine::Get(), Switch);
|
||||
@@ -67,6 +166,44 @@ bool UDirectiveUtilFunctionLibrary::GetCommandLineOption(const FString& Key, FSt
|
||||
return GetCommandLineOption(FCommandLine::Get(), Key, OutValue);
|
||||
}
|
||||
|
||||
bool UDirectiveUtilFunctionLibrary::StartStopwatch(const FName Key, const bool bRestartIfRunning)
|
||||
{
|
||||
if (Key.IsNone())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FScopeLock Lock(&StopwatchState.Lock);
|
||||
if (!bRestartIfRunning && StopwatchState.StartTimesByKey.Contains(Key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
StopwatchState.StartTimesByKey.Add(Key, FPlatformTime::Seconds());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UDirectiveUtilFunctionLibrary::StopStopwatch(const FName Key, double& ElapsedMilliseconds)
|
||||
{
|
||||
ElapsedMilliseconds = 0.0;
|
||||
if (Key.IsNone())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
double StartTime = 0.0;
|
||||
{
|
||||
FScopeLock Lock(&StopwatchState.Lock);
|
||||
if (!StopwatchState.StartTimesByKey.RemoveAndCopyValue(Key, StartTime))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ElapsedMilliseconds = FMath::Max((FPlatformTime::Seconds() - StartTime) * 1000.0, 0.0);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(const TCHAR* CommandLine, const FString& Switch)
|
||||
{
|
||||
if (Switch.IsEmpty())
|
||||
|
||||
@@ -27,13 +27,26 @@ FGameplayTagContainer UDirectiveUtilGameplayTagFunctionLibrary::GetTagParents(co
|
||||
|
||||
int32 UDirectiveUtilGameplayTagFunctionLibrary::GetTagDepth(const FGameplayTag& Tag)
|
||||
{
|
||||
return GetTagSegments(Tag).Num();
|
||||
int32 Depth = 0;
|
||||
for (FGameplayTag Current = Tag; Current.IsValid(); Current = Current.RequestDirectParent())
|
||||
{
|
||||
++Depth;
|
||||
}
|
||||
return Depth;
|
||||
}
|
||||
|
||||
FString UDirectiveUtilGameplayTagFunctionLibrary::GetTagLeafName(const FGameplayTag& Tag)
|
||||
{
|
||||
const TArray<FString> Segments = GetTagSegments(Tag);
|
||||
return Segments.Num() > 0 ? Segments.Last() : FString();
|
||||
if (!Tag.IsValid())
|
||||
{
|
||||
return FString();
|
||||
}
|
||||
|
||||
const FString TagString = Tag.GetTagName().ToString();
|
||||
int32 SeparatorIndex = INDEX_NONE;
|
||||
return TagString.FindLastChar(TEXT('.'), SeparatorIndex)
|
||||
? TagString.Mid(SeparatorIndex + 1)
|
||||
: TagString;
|
||||
}
|
||||
|
||||
TArray<FString> UDirectiveUtilGameplayTagFunctionLibrary::GetTagSegments(const FGameplayTag& Tag)
|
||||
@@ -64,10 +77,9 @@ FGameplayTagContainer UDirectiveUtilGameplayTagFunctionLibrary::GetTagDirectChil
|
||||
return DirectChildren;
|
||||
}
|
||||
|
||||
const int32 DirectChildDepth = GetTagDepth(Tag) + 1;
|
||||
for (const FGameplayTag& Child : GetTagChildren(Tag))
|
||||
{
|
||||
if (GetTagDepth(Child) == DirectChildDepth)
|
||||
if (Child.RequestDirectParent() == Tag)
|
||||
{
|
||||
DirectChildren.AddTag(Child);
|
||||
}
|
||||
@@ -77,29 +89,31 @@ FGameplayTagContainer UDirectiveUtilGameplayTagFunctionLibrary::GetTagDirectChil
|
||||
|
||||
FGameplayTag UDirectiveUtilGameplayTagFunctionLibrary::GetTagCommonAncestor(const FGameplayTag& TagA, const FGameplayTag& TagB)
|
||||
{
|
||||
const TArray<FString> SegmentsA = GetTagSegments(TagA);
|
||||
const TArray<FString> SegmentsB = GetTagSegments(TagB);
|
||||
|
||||
FString Prefix;
|
||||
for (int32 Index = 0; Index < SegmentsA.Num() && Index < SegmentsB.Num(); ++Index)
|
||||
{
|
||||
if (!SegmentsA[Index].Equals(SegmentsB[Index]))
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (!Prefix.IsEmpty())
|
||||
{
|
||||
Prefix += TEXT(".");
|
||||
}
|
||||
Prefix += SegmentsA[Index];
|
||||
}
|
||||
|
||||
if (Prefix.IsEmpty())
|
||||
if (!TagA.IsValid() || !TagB.IsValid())
|
||||
{
|
||||
return FGameplayTag();
|
||||
}
|
||||
// A common prefix of two registered tags is itself registered (parents auto-register).
|
||||
return FGameplayTag::RequestGameplayTag(FName(*Prefix), false);
|
||||
|
||||
FGameplayTag AncestorA = TagA;
|
||||
FGameplayTag AncestorB = TagB;
|
||||
int32 DepthA = GetTagDepth(AncestorA);
|
||||
int32 DepthB = GetTagDepth(AncestorB);
|
||||
while (DepthA > DepthB)
|
||||
{
|
||||
AncestorA = AncestorA.RequestDirectParent();
|
||||
--DepthA;
|
||||
}
|
||||
while (DepthB > DepthA)
|
||||
{
|
||||
AncestorB = AncestorB.RequestDirectParent();
|
||||
--DepthB;
|
||||
}
|
||||
while (AncestorA.IsValid() && AncestorB.IsValid() && AncestorA != AncestorB)
|
||||
{
|
||||
AncestorA = AncestorA.RequestDirectParent();
|
||||
AncestorB = AncestorB.RequestDirectParent();
|
||||
}
|
||||
return AncestorA == AncestorB ? AncestorA : FGameplayTag();
|
||||
}
|
||||
|
||||
FGameplayTag UDirectiveUtilGameplayTagFunctionLibrary::GetTagAtDepth(const FGameplayTag& Tag, const int32 Depth)
|
||||
@@ -109,20 +123,18 @@ FGameplayTag UDirectiveUtilGameplayTagFunctionLibrary::GetTagAtDepth(const FGame
|
||||
return FGameplayTag();
|
||||
}
|
||||
|
||||
const TArray<FString> Segments = GetTagSegments(Tag);
|
||||
if (Depth >= Segments.Num())
|
||||
const int32 TagDepth = GetTagDepth(Tag);
|
||||
if (Depth >= TagDepth)
|
||||
{
|
||||
return Tag;
|
||||
}
|
||||
|
||||
FString Prefix = Segments[0];
|
||||
for (int32 Index = 1; Index < Depth; ++Index)
|
||||
FGameplayTag Ancestor = Tag;
|
||||
for (int32 CurrentDepth = TagDepth; CurrentDepth > Depth; --CurrentDepth)
|
||||
{
|
||||
Prefix += TEXT(".");
|
||||
Prefix += Segments[Index];
|
||||
Ancestor = Ancestor.RequestDirectParent();
|
||||
}
|
||||
// An ancestor of a registered tag is always registered itself (parents auto-register).
|
||||
return FGameplayTag::RequestGameplayTag(FName(*Prefix), false);
|
||||
return Ancestor;
|
||||
}
|
||||
|
||||
FGameplayTagContainer UDirectiveUtilGameplayTagFunctionLibrary::GetTagSiblings(const FGameplayTag& Tag)
|
||||
|
||||
@@ -77,8 +77,11 @@ EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::AddInputMappingC
|
||||
|
||||
if (FailedIndices.Num() > 0)
|
||||
{
|
||||
FString FailedIndicesStr = FString::JoinBy(FailedIndices, TEXT(", "), [](const int32 Index) { return FString::Printf(TEXT("%d"), Index); });
|
||||
UE_LOGFMT(LogDirectiveUtil, Warning, "{FailedIndicies} Input Mapping Contexts failed to load and were not added! The failed indexes are [{FailedIndicieIndexes}]", FailedIndices.Num(), FailedIndicesStr);
|
||||
const FString FailedIndicesString = FString::JoinBy(
|
||||
FailedIndices, TEXT(", "), [](const int32 Index) { return FString::FromInt(Index); });
|
||||
UE_LOGFMT(LogDirectiveUtil, Warning,
|
||||
"{FailedContextCount} input mapping contexts could not be loaded. Indexes: [{FailedContextIndexes}]",
|
||||
FailedIndices.Num(), FailedIndicesString);
|
||||
}
|
||||
|
||||
if (LoadedContexts.IsEmpty())
|
||||
@@ -104,7 +107,7 @@ EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::RemoveInputMappi
|
||||
AController* PlayerController,
|
||||
const TArray<TSoftObjectPtr<UInputMappingContext>>& Contexts)
|
||||
{
|
||||
if (Contexts.IsEmpty()) { return EDirectiveUtilSuccessStatus::Failure;; }
|
||||
if (Contexts.IsEmpty()) { return EDirectiveUtilSuccessStatus::Failure; }
|
||||
|
||||
UEnhancedInputLocalPlayerSubsystem* EnhancedInput;
|
||||
const bool bEnhancedInputRetrievedFromController = TryGetEnhancedInputSubsystemFromController(PlayerController, EnhancedInput);
|
||||
@@ -118,10 +121,11 @@ EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::RemoveInputMappi
|
||||
for (int32 Index = 0; Index < Contexts.Num(); ++Index)
|
||||
{
|
||||
const TSoftObjectPtr<UInputMappingContext>& Context = Contexts[Index];
|
||||
if (const UInputMappingContext* MappingContext = Context.LoadSynchronous())
|
||||
if (const UInputMappingContext* MappingContext = Context.Get())
|
||||
{
|
||||
EnhancedInput->RemoveMappingContext(MappingContext);
|
||||
} else
|
||||
}
|
||||
else
|
||||
{
|
||||
FailedIndices.Add(Index);
|
||||
}
|
||||
@@ -129,8 +133,11 @@ EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::RemoveInputMappi
|
||||
|
||||
if (FailedIndices.Num() > 0)
|
||||
{
|
||||
FString FailedIndicesStr = FString::JoinBy(FailedIndices, TEXT(", "), [](const int32 Index) { return FString::Printf(TEXT("%d"), Index); });
|
||||
UE_LOGFMT(LogDirectiveUtil, Warning, "{FailedIndicies} Input Mapping Contexts failed to load and were not removed! The failed indexes are [{FailedIndicieIndexes}]", FailedIndices.Num(), FailedIndicesStr);
|
||||
const FString FailedIndicesString = FString::JoinBy(
|
||||
FailedIndices, TEXT(", "), [](const int32 Index) { return FString::FromInt(Index); });
|
||||
UE_LOGFMT(LogDirectiveUtil, Warning,
|
||||
"{FailedContextCount} input mapping contexts were not loaded and could not be removed. Indexes: [{FailedContextIndexes}]",
|
||||
FailedIndices.Num(), FailedIndicesString);
|
||||
}
|
||||
|
||||
if (FailedIndices.Num() == Contexts.Num())
|
||||
@@ -143,28 +150,27 @@ EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::RemoveInputMappi
|
||||
}
|
||||
|
||||
EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::SwapInputMappingContexts(
|
||||
AController* PlayerController,
|
||||
const TSoftObjectPtr<UInputMappingContext> PreviousContext,
|
||||
const TSoftObjectPtr<UInputMappingContext> NewContext,
|
||||
const int32 Priority,
|
||||
const bool bUsePreviousPriority)
|
||||
AController* PlayerController,
|
||||
const TSoftObjectPtr<UInputMappingContext> PreviousContext,
|
||||
const TSoftObjectPtr<UInputMappingContext> NewContext,
|
||||
const int32 Priority,
|
||||
const bool bUsePreviousPriority)
|
||||
{
|
||||
const UInputMappingContext* LoadedPreviousMappingContext = PreviousContext.LoadSynchronous();
|
||||
const UInputMappingContext* LoadedNewMappingContext = NewContext.LoadSynchronous();
|
||||
|
||||
if (!LoadedPreviousMappingContext || !LoadedNewMappingContext)
|
||||
{
|
||||
UE_LOGFMT(LogDirectiveUtil, Warning, "Both the previous and new input mapping contexts must be valid.");
|
||||
return EDirectiveUtilSuccessStatus::Failure;
|
||||
}
|
||||
|
||||
UEnhancedInputLocalPlayerSubsystem* EnhancedInput;
|
||||
if (!TryGetEnhancedInputSubsystemFromController(PlayerController, EnhancedInput))
|
||||
{
|
||||
return EDirectiveUtilSuccessStatus::Failure;
|
||||
}
|
||||
|
||||
if (int32 PreviousPriority; EnhancedInput->HasMappingContext(LoadedPreviousMappingContext, PreviousPriority))
|
||||
const UInputMappingContext* LoadedNewMappingContext = NewContext.LoadSynchronous();
|
||||
if (!LoadedNewMappingContext)
|
||||
{
|
||||
UE_LOGFMT(LogDirectiveUtil, Warning, "The new input mapping context could not be loaded.");
|
||||
return EDirectiveUtilSuccessStatus::Failure;
|
||||
}
|
||||
const UInputMappingContext* LoadedPreviousMappingContext = PreviousContext.Get();
|
||||
|
||||
if (int32 PreviousPriority; LoadedPreviousMappingContext && EnhancedInput->HasMappingContext(LoadedPreviousMappingContext, PreviousPriority))
|
||||
{
|
||||
const int32 TargetPriority = bUsePreviousPriority ? PreviousPriority : Priority;
|
||||
EnhancedInput->RemoveMappingContext(LoadedPreviousMappingContext);
|
||||
@@ -174,7 +180,7 @@ EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::SwapInputMapping
|
||||
else
|
||||
{
|
||||
EnhancedInput->AddMappingContext(LoadedNewMappingContext, Priority);
|
||||
UE_LOGFMT(LogDirectiveUtil, Warning, "Previous input mapping context {PreviousContext} not found. New context {NewContext} added at priority {BackupPriority}.", LoadedPreviousMappingContext->GetName(), LoadedNewMappingContext->GetName(), Priority);
|
||||
UE_LOGFMT(LogDirectiveUtil, Verbose, "Previous input mapping context was not active. New context {NewContext} added at priority {BackupPriority}.", LoadedNewMappingContext->GetName(), Priority);
|
||||
}
|
||||
|
||||
return EDirectiveUtilSuccessStatus::Success;
|
||||
@@ -189,7 +195,7 @@ UEnhancedInputLocalPlayerSubsystem* UDirectiveUtilInputFunctionLibrary::GetEnhan
|
||||
|
||||
bool UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(AController* PlayerController, TSoftObjectPtr<UInputMappingContext> Context)
|
||||
{
|
||||
const UInputMappingContext* MappingContext = Context.LoadSynchronous();
|
||||
const UInputMappingContext* MappingContext = Context.Get();
|
||||
if (!MappingContext)
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -179,7 +179,6 @@ void UDirectiveUtilMapFunctionLibrary::GenericMap_Append(
|
||||
return;
|
||||
}
|
||||
|
||||
// Appending a map onto itself is a no-op; bail before AddPair can reallocate under the source pointers.
|
||||
if (TargetMap == SourceMap)
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilMathFunctionLibrary.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
double EaseBackIn(double t)
|
||||
{
|
||||
if (t <= 0.0) { return 0.0; }
|
||||
if (t >= 1.0) { return 1.0; }
|
||||
const double s = 1.70158;
|
||||
return t * t * ((s + 1.0) * t - s);
|
||||
}
|
||||
|
||||
double EaseBackOut(double t)
|
||||
{
|
||||
if (t <= 0.0) { return 0.0; }
|
||||
if (t >= 1.0) { return 1.0; }
|
||||
const double s = 1.70158;
|
||||
t -= 1.0;
|
||||
return t * t * ((s + 1.0) * t + s) + 1.0;
|
||||
}
|
||||
|
||||
double EaseBackInOut(double t)
|
||||
{
|
||||
if (t <= 0.0) { return 0.0; }
|
||||
if (t >= 1.0) { return 1.0; }
|
||||
const double s = 1.70158 * 1.525;
|
||||
t *= 2.0;
|
||||
if (t < 1.0)
|
||||
{
|
||||
return 0.5 * (t * t * ((s + 1.0) * t - s));
|
||||
}
|
||||
t -= 2.0;
|
||||
return 0.5 * (t * t * ((s + 1.0) * t + s) + 2.0);
|
||||
}
|
||||
|
||||
double EaseElasticIn(double t)
|
||||
{
|
||||
if (t <= 0.0) { return 0.0; }
|
||||
if (t >= 1.0) { return 1.0; }
|
||||
const double p = 0.3;
|
||||
const double s = p / 4.0;
|
||||
t -= 1.0;
|
||||
return -(FMath::Pow(2.0, 10.0 * t) * FMath::Sin((t - s) * (2.0 * PI) / p));
|
||||
}
|
||||
|
||||
double EaseElasticOut(double t)
|
||||
{
|
||||
if (t <= 0.0) { return 0.0; }
|
||||
if (t >= 1.0) { return 1.0; }
|
||||
const double p = 0.3;
|
||||
const double s = p / 4.0;
|
||||
return FMath::Pow(2.0, -10.0 * t) * FMath::Sin((t - s) * (2.0 * PI) / p) + 1.0;
|
||||
}
|
||||
|
||||
double EaseElasticInOut(double t)
|
||||
{
|
||||
if (t <= 0.0) { return 0.0; }
|
||||
if (t >= 1.0) { return 1.0; }
|
||||
const double p = 0.3 * 1.5;
|
||||
const double s = p / 4.0;
|
||||
t *= 2.0;
|
||||
if (t < 1.0)
|
||||
{
|
||||
t -= 1.0;
|
||||
return -0.5 * (FMath::Pow(2.0, 10.0 * t) * FMath::Sin((t - s) * (2.0 * PI) / p));
|
||||
}
|
||||
t -= 1.0;
|
||||
return FMath::Pow(2.0, -10.0 * t) * FMath::Sin((t - s) * (2.0 * PI) / p) * 0.5 + 1.0;
|
||||
}
|
||||
|
||||
double EaseBounceOut(double t)
|
||||
{
|
||||
const double n1 = 7.5625;
|
||||
const double d1 = 2.75;
|
||||
if (t < 1.0 / d1)
|
||||
{
|
||||
return n1 * t * t;
|
||||
}
|
||||
if (t < 2.0 / d1)
|
||||
{
|
||||
t -= 1.5 / d1;
|
||||
return n1 * t * t + 0.75;
|
||||
}
|
||||
if (t < 2.5 / d1)
|
||||
{
|
||||
t -= 2.25 / d1;
|
||||
return n1 * t * t + 0.9375;
|
||||
}
|
||||
t -= 2.625 / d1;
|
||||
return n1 * t * t + 0.984375;
|
||||
}
|
||||
|
||||
double EaseBounceIn(double t)
|
||||
{
|
||||
return 1.0 - EaseBounceOut(1.0 - t);
|
||||
}
|
||||
|
||||
double EaseBounceInOut(double t)
|
||||
{
|
||||
return t < 0.5
|
||||
? (1.0 - EaseBounceOut(1.0 - 2.0 * t)) * 0.5
|
||||
: (1.0 + EaseBounceOut(2.0 * t - 1.0)) * 0.5;
|
||||
}
|
||||
|
||||
|
||||
FTransform BlendEasedTransforms(const FTransform& A, const FTransform& B, const double Alpha)
|
||||
{
|
||||
FQuat Rotation = FQuat::Slerp(A.GetRotation(), B.GetRotation(), Alpha);
|
||||
Rotation.Normalize();
|
||||
return FTransform(Rotation,
|
||||
FMath::Lerp(A.GetLocation(), B.GetLocation(), Alpha),
|
||||
FMath::Lerp(A.GetScale3D(), B.GetScale3D(), Alpha));
|
||||
}
|
||||
|
||||
template <typename ValueType, typename BlendType>
|
||||
TArray<ValueType> EaseArrays(const TArray<ValueType>& From, const TArray<ValueType>& To, const float Alpha,
|
||||
const EDirectiveUtilEaseType EaseType, const TArray<float>& PerElementAlphas, BlendType Blend)
|
||||
{
|
||||
const bool bPerElement = !PerElementAlphas.IsEmpty();
|
||||
if (From.Num() != To.Num() || !FMath::IsFinite(Alpha)
|
||||
|| (bPerElement && PerElementAlphas.Num() != From.Num()))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
const float SharedEasedAlpha = UDirectiveUtilMathFunctionLibrary::EaseAlpha(Alpha, EaseType);
|
||||
TArray<ValueType> Result;
|
||||
Result.SetNumUninitialized(From.Num());
|
||||
for (int32 Index = 0; Index < From.Num(); ++Index)
|
||||
{
|
||||
float EasedAlpha = SharedEasedAlpha;
|
||||
if (bPerElement)
|
||||
{
|
||||
if (!FMath::IsFinite(PerElementAlphas[Index]))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
EasedAlpha = UDirectiveUtilMathFunctionLibrary::EaseAlpha(PerElementAlphas[Index], EaseType);
|
||||
}
|
||||
if (From[Index].ContainsNaN() || To[Index].ContainsNaN())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
Result[Index] = Blend(From[Index], To[Index], static_cast<double>(EasedAlpha));
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
float UDirectiveUtilMathFunctionLibrary::EaseAlpha(const float Alpha, const EDirectiveUtilEaseType EaseType)
|
||||
{
|
||||
const double t = static_cast<double>(FMath::Clamp(Alpha, 0.0f, 1.0f));
|
||||
switch (EaseType)
|
||||
{
|
||||
case EDirectiveUtilEaseType::BackIn: return static_cast<float>(EaseBackIn(t));
|
||||
case EDirectiveUtilEaseType::BackOut: return static_cast<float>(EaseBackOut(t));
|
||||
case EDirectiveUtilEaseType::BackInOut: return static_cast<float>(EaseBackInOut(t));
|
||||
case EDirectiveUtilEaseType::ElasticIn: return static_cast<float>(EaseElasticIn(t));
|
||||
case EDirectiveUtilEaseType::ElasticOut: return static_cast<float>(EaseElasticOut(t));
|
||||
case EDirectiveUtilEaseType::ElasticInOut: return static_cast<float>(EaseElasticInOut(t));
|
||||
case EDirectiveUtilEaseType::BounceIn: return static_cast<float>(EaseBounceIn(t));
|
||||
case EDirectiveUtilEaseType::BounceOut: return static_cast<float>(EaseBounceOut(t));
|
||||
case EDirectiveUtilEaseType::BounceInOut: return static_cast<float>(EaseBounceInOut(t));
|
||||
case EDirectiveUtilEaseType::Linear:
|
||||
default: return static_cast<float>(t);
|
||||
}
|
||||
}
|
||||
|
||||
float UDirectiveUtilMathFunctionLibrary::EaseFloat(const float A, const float B, const float Alpha, const EDirectiveUtilEaseType EaseType)
|
||||
{
|
||||
return FMath::Lerp(A, B, EaseAlpha(Alpha, EaseType));
|
||||
}
|
||||
|
||||
FVector UDirectiveUtilMathFunctionLibrary::EaseVector(const FVector& A, const FVector& B, const float Alpha, const EDirectiveUtilEaseType EaseType)
|
||||
{
|
||||
return FMath::Lerp(A, B, static_cast<double>(EaseAlpha(Alpha, EaseType)));
|
||||
}
|
||||
|
||||
FRotator UDirectiveUtilMathFunctionLibrary::EaseRotator(const FRotator& A, const FRotator& B, const float Alpha, const EDirectiveUtilEaseType EaseType)
|
||||
{
|
||||
return FQuat::Slerp(A.Quaternion(), B.Quaternion(), EaseAlpha(Alpha, EaseType)).Rotator();
|
||||
}
|
||||
|
||||
FLinearColor UDirectiveUtilMathFunctionLibrary::EaseColor(const FLinearColor& A, const FLinearColor& B, const float Alpha, const EDirectiveUtilEaseType EaseType)
|
||||
{
|
||||
return FMath::Lerp(A, B, EaseAlpha(Alpha, EaseType));
|
||||
}
|
||||
|
||||
FTransform UDirectiveUtilMathFunctionLibrary::EaseTransform(const FTransform& A, const FTransform& B,
|
||||
const float Alpha, const EDirectiveUtilEaseType EaseType)
|
||||
{
|
||||
return BlendEasedTransforms(A, B, static_cast<double>(EaseAlpha(Alpha, EaseType)));
|
||||
}
|
||||
|
||||
TArray<FVector> UDirectiveUtilMathFunctionLibrary::EaseLocationArrays(const TArray<FVector>& From,
|
||||
const TArray<FVector>& To, const float Alpha, const EDirectiveUtilEaseType EaseType,
|
||||
const TArray<float>& PerElementAlphas)
|
||||
{
|
||||
return EaseArrays(From, To, Alpha, EaseType, PerElementAlphas,
|
||||
[](const FVector& A, const FVector& B, const double EasedAlpha)
|
||||
{
|
||||
return FMath::Lerp(A, B, EasedAlpha);
|
||||
});
|
||||
}
|
||||
|
||||
TArray<FTransform> UDirectiveUtilMathFunctionLibrary::EaseTransformArrays(const TArray<FTransform>& From,
|
||||
const TArray<FTransform>& To, const float Alpha, const EDirectiveUtilEaseType EaseType,
|
||||
const TArray<float>& PerElementAlphas)
|
||||
{
|
||||
return EaseArrays(From, To, Alpha, EaseType, PerElementAlphas, &BlendEasedTransforms);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,219 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilMathFunctionLibrary.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
float GetUsableRandomWeight(const float Weight)
|
||||
{
|
||||
return FMath::IsFinite(Weight) && Weight > 0.0f ? Weight : 0.0f;
|
||||
}
|
||||
|
||||
FVector2D MakeRandomPointInAnnulus(const double InnerRadius, const double OuterRadius,
|
||||
const double AngleSample, const double RadiusSample)
|
||||
{
|
||||
const double Angle = AngleSample * UE_TWO_PI;
|
||||
const double Radius = FMath::Sqrt(FMath::Lerp(
|
||||
InnerRadius * InnerRadius,
|
||||
OuterRadius * OuterRadius,
|
||||
RadiusSample));
|
||||
return FVector2D(FMath::Cos(Angle) * Radius, FMath::Sin(Angle) * Radius);
|
||||
}
|
||||
|
||||
template <typename RandomFractionFunction>
|
||||
FVector MakeRandomPointInSphere(const double Radius, RandomFractionFunction&& RandomFraction)
|
||||
{
|
||||
FVector Point;
|
||||
double SizeSquared;
|
||||
do
|
||||
{
|
||||
const double X = static_cast<double>(RandomFraction()) * 2.0 - 1.0;
|
||||
const double Y = static_cast<double>(RandomFraction()) * 2.0 - 1.0;
|
||||
const double Z = static_cast<double>(RandomFraction()) * 2.0 - 1.0;
|
||||
Point = FVector(X, Y, Z);
|
||||
SizeSquared = Point.SizeSquared();
|
||||
}
|
||||
while (SizeSquared > 1.0);
|
||||
|
||||
return Point * Radius;
|
||||
}
|
||||
}
|
||||
|
||||
int32 UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights(const TArray<float>& Weights)
|
||||
{
|
||||
double Total = 0.0;
|
||||
for (const float Weight : Weights)
|
||||
{
|
||||
Total += GetUsableRandomWeight(Weight);
|
||||
}
|
||||
|
||||
if (Total <= 0.0)
|
||||
{
|
||||
return INDEX_NONE;
|
||||
}
|
||||
|
||||
const double Roll = static_cast<double>(FMath::FRand()) * Total;
|
||||
double Accumulated = 0.0;
|
||||
int32 LastPositiveIndex = INDEX_NONE;
|
||||
for (int32 Index = 0; Index < Weights.Num(); ++Index)
|
||||
{
|
||||
const float Weight = GetUsableRandomWeight(Weights[Index]);
|
||||
if (Weight <= 0.0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
LastPositiveIndex = Index;
|
||||
Accumulated += Weight;
|
||||
if (Roll < Accumulated)
|
||||
{
|
||||
return Index;
|
||||
}
|
||||
}
|
||||
|
||||
return LastPositiveIndex;
|
||||
}
|
||||
|
||||
int32 UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeightsFromStream(FRandomStream& Stream, const TArray<float>& Weights)
|
||||
{
|
||||
double Total = 0.0;
|
||||
for (const float Weight : Weights)
|
||||
{
|
||||
Total += GetUsableRandomWeight(Weight);
|
||||
}
|
||||
|
||||
if (Total <= 0.0)
|
||||
{
|
||||
return INDEX_NONE;
|
||||
}
|
||||
|
||||
const double Roll = static_cast<double>(Stream.FRand()) * Total;
|
||||
double Accumulated = 0.0;
|
||||
int32 LastPositiveIndex = INDEX_NONE;
|
||||
for (int32 Index = 0; Index < Weights.Num(); ++Index)
|
||||
{
|
||||
const float Weight = GetUsableRandomWeight(Weights[Index]);
|
||||
if (Weight <= 0.0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
LastPositiveIndex = Index;
|
||||
Accumulated += Weight;
|
||||
if (Roll < Accumulated)
|
||||
{
|
||||
return Index;
|
||||
}
|
||||
}
|
||||
|
||||
return LastPositiveIndex;
|
||||
}
|
||||
|
||||
FVector2D UDirectiveUtilMathFunctionLibrary::RandomPointInCircle(const float Radius)
|
||||
{
|
||||
if (!FMath::IsFinite(Radius))
|
||||
{
|
||||
return FVector2D::ZeroVector;
|
||||
}
|
||||
|
||||
const double AbsoluteRadius = FMath::Abs(static_cast<double>(Radius));
|
||||
if (AbsoluteRadius == 0.0)
|
||||
{
|
||||
return FVector2D::ZeroVector;
|
||||
}
|
||||
const double AngleSample = FMath::FRand();
|
||||
const double RadiusSample = FMath::FRand();
|
||||
return MakeRandomPointInAnnulus(0.0, AbsoluteRadius, AngleSample, RadiusSample);
|
||||
}
|
||||
|
||||
FVector2D UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream(FRandomStream& Stream, const float Radius)
|
||||
{
|
||||
if (!FMath::IsFinite(Radius))
|
||||
{
|
||||
return FVector2D::ZeroVector;
|
||||
}
|
||||
|
||||
const double AbsoluteRadius = FMath::Abs(static_cast<double>(Radius));
|
||||
if (AbsoluteRadius == 0.0)
|
||||
{
|
||||
return FVector2D::ZeroVector;
|
||||
}
|
||||
const double AngleSample = Stream.FRand();
|
||||
const double RadiusSample = Stream.FRand();
|
||||
return MakeRandomPointInAnnulus(0.0, AbsoluteRadius, AngleSample, RadiusSample);
|
||||
}
|
||||
|
||||
FVector2D UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulus(const float InnerRadius, const float OuterRadius)
|
||||
{
|
||||
if (!FMath::IsFinite(InnerRadius) || !FMath::IsFinite(OuterRadius))
|
||||
{
|
||||
return FVector2D::ZeroVector;
|
||||
}
|
||||
|
||||
const double FirstRadius = FMath::Abs(static_cast<double>(InnerRadius));
|
||||
const double SecondRadius = FMath::Abs(static_cast<double>(OuterRadius));
|
||||
const double Inner = FMath::Min(FirstRadius, SecondRadius);
|
||||
const double Outer = FMath::Max(FirstRadius, SecondRadius);
|
||||
if (Outer == 0.0)
|
||||
{
|
||||
return FVector2D::ZeroVector;
|
||||
}
|
||||
const double AngleSample = FMath::FRand();
|
||||
const double RadiusSample = FMath::FRand();
|
||||
return MakeRandomPointInAnnulus(Inner, Outer, AngleSample, RadiusSample);
|
||||
}
|
||||
|
||||
FVector2D UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream(FRandomStream& Stream,
|
||||
const float InnerRadius, const float OuterRadius)
|
||||
{
|
||||
if (!FMath::IsFinite(InnerRadius) || !FMath::IsFinite(OuterRadius))
|
||||
{
|
||||
return FVector2D::ZeroVector;
|
||||
}
|
||||
|
||||
const double FirstRadius = FMath::Abs(static_cast<double>(InnerRadius));
|
||||
const double SecondRadius = FMath::Abs(static_cast<double>(OuterRadius));
|
||||
const double Inner = FMath::Min(FirstRadius, SecondRadius);
|
||||
const double Outer = FMath::Max(FirstRadius, SecondRadius);
|
||||
if (Outer == 0.0)
|
||||
{
|
||||
return FVector2D::ZeroVector;
|
||||
}
|
||||
const double AngleSample = Stream.FRand();
|
||||
const double RadiusSample = Stream.FRand();
|
||||
return MakeRandomPointInAnnulus(Inner, Outer, AngleSample, RadiusSample);
|
||||
}
|
||||
|
||||
FVector UDirectiveUtilMathFunctionLibrary::RandomPointInSphere(const float Radius)
|
||||
{
|
||||
if (!FMath::IsFinite(Radius))
|
||||
{
|
||||
return FVector::ZeroVector;
|
||||
}
|
||||
|
||||
const double AbsoluteRadius = FMath::Abs(static_cast<double>(Radius));
|
||||
if (AbsoluteRadius == 0.0)
|
||||
{
|
||||
return FVector::ZeroVector;
|
||||
}
|
||||
return MakeRandomPointInSphere(AbsoluteRadius, []
|
||||
{
|
||||
return FMath::FRand();
|
||||
});
|
||||
}
|
||||
|
||||
FVector UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream(FRandomStream& Stream, const float Radius)
|
||||
{
|
||||
if (!FMath::IsFinite(Radius))
|
||||
{
|
||||
return FVector::ZeroVector;
|
||||
}
|
||||
|
||||
const double AbsoluteRadius = FMath::Abs(static_cast<double>(Radius));
|
||||
if (AbsoluteRadius == 0.0)
|
||||
{
|
||||
return FVector::ZeroVector;
|
||||
}
|
||||
return MakeRandomPointInSphere(AbsoluteRadius, [&Stream]
|
||||
{
|
||||
return Stream.FRand();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,576 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Libraries/DirectiveUtilMathFunctionLibrary.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace
|
||||
{
|
||||
float GetUsableStatisticsWeight(const float Weight)
|
||||
{
|
||||
return FMath::IsFinite(Weight) && Weight > 0.0f ? Weight : 0.0f;
|
||||
}
|
||||
|
||||
template <typename ValueType>
|
||||
ValueType SelectStatisticsNth(TArray<ValueType>& Values, const int32 NthIndex)
|
||||
{
|
||||
int32 Left = 0;
|
||||
int32 Right = Values.Num() - 1;
|
||||
int32 RemainingDepth = FMath::FloorLog2(static_cast<uint32>(Values.Num())) * 2;
|
||||
while (Left < Right)
|
||||
{
|
||||
if (RemainingDepth-- <= 0)
|
||||
{
|
||||
Values.Sort();
|
||||
return Values[NthIndex];
|
||||
}
|
||||
|
||||
const int32 Middle = Left + (Right - Left) / 2;
|
||||
if (Values[Middle] < Values[Left])
|
||||
{
|
||||
Values.Swap(Middle, Left);
|
||||
}
|
||||
if (Values[Right] < Values[Left])
|
||||
{
|
||||
Values.Swap(Right, Left);
|
||||
}
|
||||
if (Values[Right] < Values[Middle])
|
||||
{
|
||||
Values.Swap(Right, Middle);
|
||||
}
|
||||
const ValueType Pivot = Values[Middle];
|
||||
|
||||
int32 LessEnd = Left;
|
||||
int32 Current = Left;
|
||||
int32 GreaterStart = Right;
|
||||
while (Current <= GreaterStart)
|
||||
{
|
||||
if (Values[Current] < Pivot)
|
||||
{
|
||||
Values.Swap(LessEnd++, Current++);
|
||||
}
|
||||
else if (Pivot < Values[Current])
|
||||
{
|
||||
Values.Swap(Current, GreaterStart--);
|
||||
}
|
||||
else
|
||||
{
|
||||
++Current;
|
||||
}
|
||||
}
|
||||
|
||||
if (NthIndex < LessEnd)
|
||||
{
|
||||
Right = LessEnd - 1;
|
||||
}
|
||||
else if (NthIndex > GreaterStart)
|
||||
{
|
||||
Left = GreaterStart + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Values[NthIndex];
|
||||
}
|
||||
}
|
||||
return Values[Left];
|
||||
}
|
||||
|
||||
template <typename ValueType>
|
||||
double CalculateStatisticsMedian(TArray<ValueType>& Values)
|
||||
{
|
||||
const int32 Middle = Values.Num() / 2;
|
||||
const ValueType UpperMiddle = SelectStatisticsNth(Values, Middle);
|
||||
if (Values.Num() % 2 != 0)
|
||||
{
|
||||
return static_cast<double>(UpperMiddle);
|
||||
}
|
||||
|
||||
ValueType LowerMiddle = Values[0];
|
||||
for (int32 Index = 1; Index < Middle; ++Index)
|
||||
{
|
||||
LowerMiddle = FMath::Max(LowerMiddle, Values[Index]);
|
||||
}
|
||||
return (static_cast<double>(LowerMiddle) + static_cast<double>(UpperMiddle)) * 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
float UDirectiveUtilMathFunctionLibrary::RoundToDecimals(const float Value, int32 Decimals)
|
||||
{
|
||||
Decimals = FMath::Clamp(Decimals, 0, 10);
|
||||
if (Decimals == 0)
|
||||
{
|
||||
return FMath::RoundHalfFromZero(Value);
|
||||
}
|
||||
const double Factor = FMath::Pow(10.0, static_cast<double>(Decimals));
|
||||
return static_cast<float>(FMath::RoundHalfFromZero(static_cast<double>(Value) * Factor) / Factor);
|
||||
}
|
||||
|
||||
FText UDirectiveUtilMathFunctionLibrary::RoundToDecimalsAsText(const float Value, int32 Decimals)
|
||||
{
|
||||
Decimals = FMath::Clamp(Decimals, 0, 10);
|
||||
FNumberFormattingOptions Options;
|
||||
Options.MinimumFractionalDigits = 0;
|
||||
Options.MaximumFractionalDigits = Decimals;
|
||||
Options.RoundingMode = ERoundingMode::HalfFromZero;
|
||||
return FText::AsNumber(Value, &Options);
|
||||
}
|
||||
|
||||
FText UDirectiveUtilMathFunctionLibrary::FormatBytes(const int64 Bytes, int32 Decimals)
|
||||
{
|
||||
Decimals = FMath::Clamp(Decimals, 0, 3);
|
||||
|
||||
static const TCHAR* Suffixes[] = { TEXT("B"), TEXT("KB"), TEXT("MB"), TEXT("GB"), TEXT("TB"), TEXT("PB") };
|
||||
const bool bNegative = Bytes < 0;
|
||||
double Value = FMath::Abs(static_cast<double>(Bytes));
|
||||
int32 SuffixIndex = 0;
|
||||
while (Value >= 1024.0 && SuffixIndex < UE_ARRAY_COUNT(Suffixes) - 1)
|
||||
{
|
||||
Value /= 1024.0;
|
||||
++SuffixIndex;
|
||||
}
|
||||
|
||||
return FText::FromString(FString::Printf(TEXT("%s%.*f %s"),
|
||||
bNegative ? TEXT("-") : TEXT(""), SuffixIndex == 0 ? 0 : Decimals, Value, Suffixes[SuffixIndex]));
|
||||
}
|
||||
|
||||
FText UDirectiveUtilMathFunctionLibrary::FormatDuration(const float Seconds, const bool bIncludeSeconds)
|
||||
{
|
||||
if (!FMath::IsFinite(Seconds))
|
||||
{
|
||||
return FText::FromString(TEXT("0s"));
|
||||
}
|
||||
|
||||
const double AbsoluteSeconds = FMath::Abs(static_cast<double>(Seconds));
|
||||
const int64 TotalSeconds = AbsoluteSeconds >= static_cast<double>(TNumericLimits<int64>::Max())
|
||||
? TNumericLimits<int64>::Max()
|
||||
: static_cast<int64>(AbsoluteSeconds);
|
||||
const int64 VisibleSeconds = bIncludeSeconds ? TotalSeconds : (TotalSeconds / 60) * 60;
|
||||
const bool bNegative = Seconds < 0.0f && VisibleSeconds > 0;
|
||||
|
||||
const int64 UnitValues[] = { TotalSeconds / 86400, (TotalSeconds / 3600) % 24, (TotalSeconds / 60) % 60, TotalSeconds % 60 };
|
||||
static const TCHAR* UnitSuffixes[] = { TEXT("d"), TEXT("h"), TEXT("m"), TEXT("s") };
|
||||
const int32 NumUnits = bIncludeSeconds ? 4 : 3;
|
||||
|
||||
int32 FirstUnit = NumUnits - 1;
|
||||
for (int32 Index = 0; Index < NumUnits; ++Index)
|
||||
{
|
||||
if (UnitValues[Index] != 0)
|
||||
{
|
||||
FirstUnit = Index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
int32 LastUnit = FirstUnit;
|
||||
for (int32 Index = NumUnits - 1; Index >= FirstUnit; --Index)
|
||||
{
|
||||
if (UnitValues[Index] != 0)
|
||||
{
|
||||
LastUnit = Index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
FString Result = bNegative ? TEXT("-") : TEXT("");
|
||||
for (int32 Index = FirstUnit; Index <= LastUnit; ++Index)
|
||||
{
|
||||
if (Index == FirstUnit)
|
||||
{
|
||||
Result += FString::Printf(TEXT("%lld%s"), UnitValues[Index], UnitSuffixes[Index]);
|
||||
}
|
||||
else
|
||||
{
|
||||
Result += FString::Printf(TEXT(" %02lld%s"), UnitValues[Index], UnitSuffixes[Index]);
|
||||
}
|
||||
}
|
||||
return FText::FromString(Result);
|
||||
}
|
||||
|
||||
FText UDirectiveUtilMathFunctionLibrary::FormatRelativeTime(const FDateTime& Timestamp)
|
||||
{
|
||||
const FTimespan Delta = FDateTime::Now() - Timestamp;
|
||||
const bool bFuture = Delta.GetTicks() < 0;
|
||||
// Round first so timestamps near the current second stay in the expected bucket.
|
||||
const int64 SecondsAbs = static_cast<int64>(FMath::RoundToDouble(FMath::Abs(Delta.GetTotalSeconds())));
|
||||
|
||||
if (SecondsAbs < 60)
|
||||
{
|
||||
return FText::FromString(TEXT("just now"));
|
||||
}
|
||||
|
||||
int64 Count;
|
||||
const TCHAR* Unit;
|
||||
if (SecondsAbs < 3600)
|
||||
{
|
||||
Count = SecondsAbs / 60;
|
||||
Unit = TEXT("minute");
|
||||
}
|
||||
else if (SecondsAbs < 86400)
|
||||
{
|
||||
Count = SecondsAbs / 3600;
|
||||
Unit = TEXT("hour");
|
||||
}
|
||||
else
|
||||
{
|
||||
Count = SecondsAbs / 86400;
|
||||
Unit = TEXT("day");
|
||||
}
|
||||
|
||||
const FString Quantity = FString::Printf(TEXT("%lld %s%s"), Count, Unit, Count == 1 ? TEXT("") : TEXT("s"));
|
||||
return FText::FromString(bFuture
|
||||
? FString::Printf(TEXT("in %s"), *Quantity)
|
||||
: FString::Printf(TEXT("%s ago"), *Quantity));
|
||||
}
|
||||
|
||||
int64 UDirectiveUtilMathFunctionLibrary::GetIntArraySum(const TArray<int32>& Values)
|
||||
{
|
||||
int64 Sum = 0;
|
||||
for (const int32 Value : Values)
|
||||
{
|
||||
Sum += Value;
|
||||
}
|
||||
return Sum;
|
||||
}
|
||||
|
||||
float UDirectiveUtilMathFunctionLibrary::GetIntArrayAverage(const TArray<int32>& Values)
|
||||
{
|
||||
if (Values.IsEmpty())
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
return static_cast<float>(static_cast<double>(GetIntArraySum(Values)) / Values.Num());
|
||||
}
|
||||
|
||||
float UDirectiveUtilMathFunctionLibrary::GetIntArrayMedian(const TArray<int32>& Values)
|
||||
{
|
||||
if (Values.IsEmpty())
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
TArray<int32> WorkingValues = Values;
|
||||
return static_cast<float>(CalculateStatisticsMedian(WorkingValues));
|
||||
}
|
||||
|
||||
float UDirectiveUtilMathFunctionLibrary::GetIntArrayStandardDeviation(const TArray<int32>& Values)
|
||||
{
|
||||
if (Values.IsEmpty())
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
const double Mean = static_cast<double>(GetIntArraySum(Values)) / Values.Num();
|
||||
double SquaredDeltaSum = 0.0;
|
||||
for (const int32 Value : Values)
|
||||
{
|
||||
const double Delta = static_cast<double>(Value) - Mean;
|
||||
SquaredDeltaSum += Delta * Delta;
|
||||
}
|
||||
return static_cast<float>(FMath::Sqrt(SquaredDeltaSum / Values.Num()));
|
||||
}
|
||||
|
||||
float UDirectiveUtilMathFunctionLibrary::GetFloatArraySum(const TArray<float>& Values)
|
||||
{
|
||||
double Sum = 0.0;
|
||||
for (const float Value : Values)
|
||||
{
|
||||
Sum += static_cast<double>(Value);
|
||||
}
|
||||
return static_cast<float>(Sum);
|
||||
}
|
||||
|
||||
float UDirectiveUtilMathFunctionLibrary::GetFloatArrayAverage(const TArray<float>& Values)
|
||||
{
|
||||
if (Values.IsEmpty())
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
double Sum = 0.0;
|
||||
for (const float Value : Values)
|
||||
{
|
||||
Sum += static_cast<double>(Value);
|
||||
}
|
||||
return static_cast<float>(Sum / Values.Num());
|
||||
}
|
||||
|
||||
float UDirectiveUtilMathFunctionLibrary::GetFloatArrayMedian(const TArray<float>& Values)
|
||||
{
|
||||
if (Values.IsEmpty())
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
TArray<float> WorkingValues = Values;
|
||||
if (WorkingValues.ContainsByPredicate([](const float Value) { return FMath::IsNaN(Value); }))
|
||||
{
|
||||
return std::numeric_limits<float>::quiet_NaN();
|
||||
}
|
||||
return static_cast<float>(CalculateStatisticsMedian(WorkingValues));
|
||||
}
|
||||
|
||||
float UDirectiveUtilMathFunctionLibrary::GetFloatArrayStandardDeviation(const TArray<float>& Values)
|
||||
{
|
||||
if (Values.IsEmpty())
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
double Sum = 0.0;
|
||||
for (const float Value : Values)
|
||||
{
|
||||
Sum += static_cast<double>(Value);
|
||||
}
|
||||
const double Mean = Sum / Values.Num();
|
||||
|
||||
double SquaredDeltaSum = 0.0;
|
||||
for (const float Value : Values)
|
||||
{
|
||||
const double Delta = static_cast<double>(Value) - Mean;
|
||||
SquaredDeltaSum += Delta * Delta;
|
||||
}
|
||||
return static_cast<float>(FMath::Sqrt(SquaredDeltaSum / Values.Num()));
|
||||
}
|
||||
|
||||
bool UDirectiveUtilMathFunctionLibrary::GetAngleArrayAverage(const TArray<float>& Angles,
|
||||
float& AverageAngle, float& ResultantStrength)
|
||||
{
|
||||
AverageAngle = 0.0f;
|
||||
ResultantStrength = 0.0f;
|
||||
if (Angles.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
double SineSum = 0.0;
|
||||
double CosineSum = 0.0;
|
||||
for (const float Angle : Angles)
|
||||
{
|
||||
if (!FMath::IsFinite(Angle))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const double Radians = FMath::DegreesToRadians(FMath::Fmod(static_cast<double>(Angle), 360.0));
|
||||
SineSum += FMath::Sin(Radians);
|
||||
CosineSum += FMath::Cos(Radians);
|
||||
}
|
||||
|
||||
const double Magnitude = FMath::Sqrt(SineSum * SineSum + CosineSum * CosineSum);
|
||||
ResultantStrength = static_cast<float>(FMath::Clamp(Magnitude / Angles.Num(), 0.0, 1.0));
|
||||
if (ResultantStrength <= UE_DOUBLE_SMALL_NUMBER)
|
||||
{
|
||||
ResultantStrength = 0.0f;
|
||||
return false;
|
||||
}
|
||||
|
||||
AverageAngle = static_cast<float>(FMath::RadiansToDegrees(FMath::Atan2(SineSum, CosineSum)));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UDirectiveUtilMathFunctionLibrary::GetWeightedFloatArrayAverage(const TArray<float>& Values,
|
||||
const TArray<float>& Weights, float& Average)
|
||||
{
|
||||
Average = 0.0f;
|
||||
if (Values.IsEmpty() || Values.Num() != Weights.Num())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
double WeightedSum = 0.0;
|
||||
double WeightSum = 0.0;
|
||||
for (int32 Index = 0; Index < Values.Num(); ++Index)
|
||||
{
|
||||
if (!FMath::IsFinite(Values[Index]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const double Weight = GetUsableStatisticsWeight(Weights[Index]);
|
||||
WeightedSum += static_cast<double>(Values[Index]) * Weight;
|
||||
WeightSum += Weight;
|
||||
}
|
||||
|
||||
if (WeightSum <= 0.0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Average = static_cast<float>(WeightedSum / WeightSum);
|
||||
return FMath::IsFinite(Average);
|
||||
}
|
||||
|
||||
bool UDirectiveUtilMathFunctionLibrary::GetWeightedVectorArrayAverage(const TArray<FVector>& Values,
|
||||
const TArray<float>& Weights, FVector& Average)
|
||||
{
|
||||
Average = FVector::ZeroVector;
|
||||
if (Values.IsEmpty() || Values.Num() != Weights.Num())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FVector RunningAverage = FVector::ZeroVector;
|
||||
double WeightSum = 0.0;
|
||||
for (int32 Index = 0; Index < Values.Num(); ++Index)
|
||||
{
|
||||
if (Values[Index].ContainsNaN())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const double Weight = GetUsableStatisticsWeight(Weights[Index]);
|
||||
if (Weight > 0.0)
|
||||
{
|
||||
const double NewWeightSum = WeightSum + Weight;
|
||||
RunningAverage = FMath::LerpStable(RunningAverage, Values[Index], Weight / NewWeightSum);
|
||||
WeightSum = NewWeightSum;
|
||||
}
|
||||
}
|
||||
|
||||
if (WeightSum <= 0.0 || RunningAverage.ContainsNaN())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Average = RunningAverage;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UDirectiveUtilMathFunctionLibrary::NormalizeFloatArrayToRange(const TArray<float>& Values,
|
||||
const float OutputMinimum, const float OutputMaximum, TArray<float>& NormalizedValues)
|
||||
{
|
||||
TArray<float> ValuesCopy;
|
||||
const TArray<float>* SourceValues = &Values;
|
||||
if (&Values == &NormalizedValues)
|
||||
{
|
||||
ValuesCopy = Values;
|
||||
SourceValues = &ValuesCopy;
|
||||
}
|
||||
|
||||
NormalizedValues.Reset();
|
||||
if (SourceValues->IsEmpty() || !FMath::IsFinite(OutputMinimum) || !FMath::IsFinite(OutputMaximum))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
float InputMinimum = (*SourceValues)[0];
|
||||
float InputMaximum = (*SourceValues)[0];
|
||||
for (const float Value : *SourceValues)
|
||||
{
|
||||
if (!FMath::IsFinite(Value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
InputMinimum = FMath::Min(InputMinimum, Value);
|
||||
InputMaximum = FMath::Max(InputMaximum, Value);
|
||||
}
|
||||
|
||||
NormalizedValues.SetNumUninitialized(SourceValues->Num());
|
||||
if (InputMinimum == InputMaximum)
|
||||
{
|
||||
NormalizedValues.Init(OutputMinimum, SourceValues->Num());
|
||||
return true;
|
||||
}
|
||||
|
||||
const double Scale = (static_cast<double>(OutputMaximum) - OutputMinimum)
|
||||
/ (static_cast<double>(InputMaximum) - InputMinimum);
|
||||
for (int32 Index = 0; Index < SourceValues->Num(); ++Index)
|
||||
{
|
||||
NormalizedValues[Index] = static_cast<float>(OutputMinimum
|
||||
+ (static_cast<double>((*SourceValues)[Index]) - InputMinimum) * Scale);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UDirectiveUtilMathFunctionLibrary::NormalizeWeights(const TArray<float>& Weights,
|
||||
TArray<float>& NormalizedWeights)
|
||||
{
|
||||
TArray<float> WeightsCopy;
|
||||
const TArray<float>* SourceWeights = &Weights;
|
||||
if (&Weights == &NormalizedWeights)
|
||||
{
|
||||
WeightsCopy = Weights;
|
||||
SourceWeights = &WeightsCopy;
|
||||
}
|
||||
|
||||
NormalizedWeights.Reset();
|
||||
if (SourceWeights->IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
double WeightSum = 0.0;
|
||||
for (const float Weight : *SourceWeights)
|
||||
{
|
||||
WeightSum += GetUsableStatisticsWeight(Weight);
|
||||
}
|
||||
if (WeightSum <= 0.0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
NormalizedWeights.SetNumUninitialized(SourceWeights->Num());
|
||||
for (int32 Index = 0; Index < SourceWeights->Num(); ++Index)
|
||||
{
|
||||
NormalizedWeights[Index] = static_cast<float>(GetUsableStatisticsWeight((*SourceWeights)[Index]) / WeightSum);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile(const TArray<float>& Values,
|
||||
const float Percentile, float& Value)
|
||||
{
|
||||
Value = 0.0f;
|
||||
if (Values.IsEmpty() || !FMath::IsFinite(Percentile))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const float Candidate : Values)
|
||||
{
|
||||
if (!FMath::IsFinite(Candidate))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
TArray<float> WorkingValues = Values;
|
||||
|
||||
const double Position = FMath::Clamp(static_cast<double>(Percentile), 0.0, 100.0)
|
||||
* 0.01 * (WorkingValues.Num() - 1);
|
||||
const int32 LowerIndex = FMath::FloorToInt(Position);
|
||||
const int32 UpperIndex = FMath::CeilToInt(Position);
|
||||
const float LowerValue = SelectStatisticsNth(WorkingValues, LowerIndex);
|
||||
if (LowerIndex == UpperIndex)
|
||||
{
|
||||
Value = LowerValue;
|
||||
return true;
|
||||
}
|
||||
const float UpperValue = SelectStatisticsNth(WorkingValues, UpperIndex);
|
||||
Value = static_cast<float>(FMath::Lerp(
|
||||
static_cast<double>(LowerValue),
|
||||
static_cast<double>(UpperValue),
|
||||
Position - LowerIndex));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UDirectiveUtilMathFunctionLibrary::GetFloatArrayRootMeanSquare(const TArray<float>& Values,
|
||||
float& RootMeanSquare)
|
||||
{
|
||||
RootMeanSquare = 0.0f;
|
||||
if (Values.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
double SquaredSum = 0.0;
|
||||
for (const float Value : Values)
|
||||
{
|
||||
if (!FMath::IsFinite(Value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
SquaredSum += static_cast<double>(Value) * Value;
|
||||
}
|
||||
|
||||
RootMeanSquare = static_cast<float>(FMath::Sqrt(SquaredSum / Values.Num()));
|
||||
return FMath::IsFinite(RootMeanSquare);
|
||||
}
|
||||
@@ -7,6 +7,10 @@
|
||||
#include "GameFramework/SaveGame.h"
|
||||
#include "HAL/FileManager.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "PlatformFeatures.h"
|
||||
#include "SaveGameSystem.h"
|
||||
|
||||
#include <ctime>
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -24,11 +28,51 @@ namespace
|
||||
{
|
||||
return UDirectiveUtilStringFunctionLibrary::IsValidFileName(SlotName);
|
||||
}
|
||||
|
||||
ISaveGameSystem* GetSaveGameSystem()
|
||||
{
|
||||
return IPlatformFeaturesModule::Get().GetSaveGameSystem();
|
||||
}
|
||||
|
||||
FDateTime ConvertUtcFileTimeToLocal(const FDateTime& UtcTimestamp)
|
||||
{
|
||||
const int64 UnixSeconds = UtcTimestamp.ToUnixTimestamp();
|
||||
const int32 Milliseconds = UtcTimestamp.GetMillisecond();
|
||||
const time_t Time = static_cast<time_t>(UnixSeconds);
|
||||
tm LocalTm;
|
||||
#if PLATFORM_WINDOWS
|
||||
if (localtime_s(&LocalTm, &Time) != 0)
|
||||
{
|
||||
return UtcTimestamp;
|
||||
}
|
||||
#else
|
||||
if (localtime_r(&Time, &LocalTm) == nullptr)
|
||||
{
|
||||
return UtcTimestamp;
|
||||
}
|
||||
#endif
|
||||
return FDateTime(
|
||||
LocalTm.tm_year + 1900,
|
||||
LocalTm.tm_mon + 1,
|
||||
LocalTm.tm_mday,
|
||||
LocalTm.tm_hour,
|
||||
LocalTm.tm_min,
|
||||
LocalTm.tm_sec,
|
||||
Milliseconds);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TArray<FString> UDirectiveUtilSaveGameFunctionLibrary::GetAllSaveSlotNames()
|
||||
{
|
||||
TArray<FString> SlotNames;
|
||||
if (ISaveGameSystem* SaveSystem = GetSaveGameSystem())
|
||||
{
|
||||
if (SaveSystem->GetSaveGameNames(SlotNames, 0))
|
||||
{
|
||||
return SlotNames;
|
||||
}
|
||||
}
|
||||
|
||||
TArray<FString> Files;
|
||||
IFileManager::Get().FindFiles(Files, *(GetSaveGamesDirectory() / TEXT("*.sav")), true, false);
|
||||
@@ -49,13 +93,21 @@ bool UDirectiveUtilSaveGameFunctionLibrary::GetSaveSlotTimestamp(const FString&
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ISaveGameSystem* SaveSystem = GetSaveGameSystem())
|
||||
{
|
||||
if (!SaveSystem->DoesSaveGameExist(*SlotName, 0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const FDateTime Timestamp = IFileManager::Get().GetTimeStamp(*GetSaveSlotFilePath(SlotName));
|
||||
if (Timestamp == FDateTime::MinValue())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
OutTimestamp = Timestamp + (FDateTime::Now() - FDateTime::UtcNow());
|
||||
OutTimestamp = ConvertUtcFileTimeToLocal(Timestamp);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -98,10 +150,30 @@ bool UDirectiveUtilSaveGameFunctionLibrary::DeleteSaveSlot(const FString& SlotNa
|
||||
|
||||
bool UDirectiveUtilSaveGameFunctionLibrary::RenameSaveSlot(const FString& OldSlotName, const FString& NewSlotName, const int32 UserIndex)
|
||||
{
|
||||
if (!IsValidSaveSlotName(OldSlotName) || !IsValidSaveSlotName(NewSlotName) || OldSlotName == NewSlotName)
|
||||
if (!IsValidSaveSlotName(OldSlotName) || !IsValidSaveSlotName(NewSlotName)
|
||||
|| OldSlotName.Equals(NewSlotName, ESearchCase::CaseSensitive))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool bCaseOnlyRename = OldSlotName.Equals(NewSlotName, ESearchCase::IgnoreCase);
|
||||
if (bCaseOnlyRename)
|
||||
{
|
||||
TArray<uint8> SaveData;
|
||||
if (!UGameplayStatics::LoadDataFromSlot(SaveData, OldSlotName, UserIndex) || SaveData.Num() == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!UGameplayStatics::SaveDataToSlot(SaveData, NewSlotName, UserIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// Slot-name case sensitivity belongs to the active platform backend. Deleting
|
||||
// the old spelling here can delete the newly written slot on a case-insensitive
|
||||
// filesystem, so treat both spellings as the same logical slot and rewrite it.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!UGameplayStatics::DoesSaveGameExist(OldSlotName, UserIndex) || UGameplayStatics::DoesSaveGameExist(NewSlotName, UserIndex))
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -14,6 +14,80 @@ namespace
|
||||
const FTCHARToUTF8 Converter(*String, String.Len());
|
||||
return TArray<uint8>(reinterpret_cast<const uint8*>(Converter.Get()), Converter.Length());
|
||||
}
|
||||
|
||||
int32 CalculateLevenshteinDistance(
|
||||
FStringView Left,
|
||||
FStringView Right,
|
||||
TArray<int32>& PreviousRow,
|
||||
TArray<int32>& CurrentRow)
|
||||
{
|
||||
int32 Start = 0;
|
||||
while (Start < Left.Len() && Start < Right.Len() && Left[Start] == Right[Start])
|
||||
{
|
||||
++Start;
|
||||
}
|
||||
|
||||
int32 LeftEnd = Left.Len();
|
||||
int32 RightEnd = Right.Len();
|
||||
while (LeftEnd > Start && RightEnd > Start && Left[LeftEnd - 1] == Right[RightEnd - 1])
|
||||
{
|
||||
--LeftEnd;
|
||||
--RightEnd;
|
||||
}
|
||||
|
||||
int32 LeftLength = LeftEnd - Start;
|
||||
int32 RightLength = RightEnd - Start;
|
||||
int32 LeftStart = Start;
|
||||
int32 RightStart = Start;
|
||||
if (RightLength > LeftLength)
|
||||
{
|
||||
Swap(Left, Right);
|
||||
Swap(LeftLength, RightLength);
|
||||
Swap(LeftStart, RightStart);
|
||||
}
|
||||
|
||||
if (RightLength == 0)
|
||||
{
|
||||
return LeftLength;
|
||||
}
|
||||
|
||||
PreviousRow.SetNumUninitialized(RightLength + 1);
|
||||
CurrentRow.SetNumUninitialized(RightLength + 1);
|
||||
for (int32 ColumnIndex = 0; ColumnIndex <= RightLength; ++ColumnIndex)
|
||||
{
|
||||
PreviousRow[ColumnIndex] = ColumnIndex;
|
||||
}
|
||||
|
||||
for (int32 RowIndex = 1; RowIndex <= LeftLength; ++RowIndex)
|
||||
{
|
||||
CurrentRow[0] = RowIndex;
|
||||
for (int32 ColumnIndex = 1; ColumnIndex <= RightLength; ++ColumnIndex)
|
||||
{
|
||||
const int32 SubstitutionCost = Left[LeftStart + RowIndex - 1] == Right[RightStart + ColumnIndex - 1] ? 0 : 1;
|
||||
CurrentRow[ColumnIndex] = FMath::Min3(
|
||||
PreviousRow[ColumnIndex] + 1,
|
||||
CurrentRow[ColumnIndex - 1] + 1,
|
||||
PreviousRow[ColumnIndex - 1] + SubstitutionCost);
|
||||
}
|
||||
Swap(PreviousRow, CurrentRow);
|
||||
}
|
||||
return PreviousRow[RightLength];
|
||||
}
|
||||
|
||||
float CalculateStringSimilarity(
|
||||
const FStringView Left,
|
||||
const FStringView Right,
|
||||
const int32 MaxLength,
|
||||
TArray<int32>& PreviousRow,
|
||||
TArray<int32>& CurrentRow)
|
||||
{
|
||||
if (MaxLength == 0)
|
||||
{
|
||||
return 1.0f;
|
||||
}
|
||||
const int32 Distance = CalculateLevenshteinDistance(Left, Right, PreviousRow, CurrentRow);
|
||||
return 1.0f - static_cast<float>(Distance) / static_cast<float>(MaxLength);
|
||||
}
|
||||
}
|
||||
|
||||
bool UDirectiveUtilStringFunctionLibrary::ContainsLetters(const FString& String)
|
||||
@@ -219,50 +293,27 @@ TArray<FString> UDirectiveUtilStringFunctionLibrary::GetSortedStringArray(const
|
||||
|
||||
int32 UDirectiveUtilStringFunctionLibrary::GetLevenshteinDistance(const FString& A, const FString& B, const bool bCaseSensitive)
|
||||
{
|
||||
const FString StringA = bCaseSensitive ? A : A.ToLower();
|
||||
const FString StringB = bCaseSensitive ? B : B.ToLower();
|
||||
const int32 LenA = StringA.Len();
|
||||
const int32 LenB = StringB.Len();
|
||||
|
||||
if (LenA == 0) { return LenB; }
|
||||
if (LenB == 0) { return LenA; }
|
||||
|
||||
const FString NormalizedA = bCaseSensitive ? FString() : A.ToLower();
|
||||
const FString NormalizedB = bCaseSensitive ? FString() : B.ToLower();
|
||||
const FStringView ViewA = bCaseSensitive ? FStringView(A) : FStringView(NormalizedA);
|
||||
const FStringView ViewB = bCaseSensitive ? FStringView(B) : FStringView(NormalizedB);
|
||||
TArray<int32> PreviousRow;
|
||||
TArray<int32> CurrentRow;
|
||||
PreviousRow.SetNumUninitialized(LenB + 1);
|
||||
CurrentRow.SetNumUninitialized(LenB + 1);
|
||||
|
||||
for (int32 ColumnIndex = 0; ColumnIndex <= LenB; ++ColumnIndex)
|
||||
{
|
||||
PreviousRow[ColumnIndex] = ColumnIndex;
|
||||
}
|
||||
|
||||
for (int32 RowIndex = 1; RowIndex <= LenA; ++RowIndex)
|
||||
{
|
||||
CurrentRow[0] = RowIndex;
|
||||
for (int32 ColumnIndex = 1; ColumnIndex <= LenB; ++ColumnIndex)
|
||||
{
|
||||
const int32 SubstitutionCost = (StringA[RowIndex - 1] == StringB[ColumnIndex - 1]) ? 0 : 1;
|
||||
CurrentRow[ColumnIndex] = FMath::Min3(
|
||||
PreviousRow[ColumnIndex] + 1,
|
||||
CurrentRow[ColumnIndex - 1] + 1,
|
||||
PreviousRow[ColumnIndex - 1] + SubstitutionCost);
|
||||
}
|
||||
Exchange(PreviousRow, CurrentRow);
|
||||
}
|
||||
|
||||
return PreviousRow[LenB];
|
||||
return CalculateLevenshteinDistance(ViewA, ViewB, PreviousRow, CurrentRow);
|
||||
}
|
||||
|
||||
float UDirectiveUtilStringFunctionLibrary::GetStringSimilarity(const FString& A, const FString& B, const bool bCaseSensitive)
|
||||
{
|
||||
const int32 MaxLength = FMath::Max(A.Len(), B.Len());
|
||||
if (MaxLength == 0)
|
||||
{
|
||||
return 1.0f;
|
||||
}
|
||||
const int32 Distance = GetLevenshteinDistance(A, B, bCaseSensitive);
|
||||
return 1.0f - (static_cast<float>(Distance) / static_cast<float>(MaxLength));
|
||||
const FString NormalizedA = bCaseSensitive ? FString() : A.ToLower();
|
||||
const FString NormalizedB = bCaseSensitive ? FString() : B.ToLower();
|
||||
TArray<int32> PreviousRow;
|
||||
TArray<int32> CurrentRow;
|
||||
return CalculateStringSimilarity(
|
||||
bCaseSensitive ? FStringView(A) : FStringView(NormalizedA),
|
||||
bCaseSensitive ? FStringView(B) : FStringView(NormalizedB),
|
||||
FMath::Max(A.Len(), B.Len()),
|
||||
PreviousRow,
|
||||
CurrentRow);
|
||||
}
|
||||
|
||||
bool UDirectiveUtilStringFunctionLibrary::ContainsAny(const FString& Source, const TArray<FString>& SearchTerms, const bool bCaseSensitive)
|
||||
@@ -394,28 +445,96 @@ int32 UDirectiveUtilStringFunctionLibrary::Crc32Bytes(const TArray<uint8>& Bytes
|
||||
return static_cast<int32>(FCrc::MemCrc32(Bytes.GetData(), Bytes.Num()));
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
bool IsReservedDeviceFileName(const FString& FileName)
|
||||
{
|
||||
FString Stem = FileName;
|
||||
int32 DotIndex = INDEX_NONE;
|
||||
if (FileName.FindChar(TEXT('.'), DotIndex))
|
||||
{
|
||||
Stem = FileName.Left(DotIndex);
|
||||
}
|
||||
|
||||
while (Stem.EndsWith(TEXT(".")) || Stem.EndsWith(TEXT(" ")))
|
||||
{
|
||||
Stem.LeftChopInline(1);
|
||||
}
|
||||
if (Stem.IsEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
static const TCHAR* ReservedNames[] = {
|
||||
TEXT("CON"), TEXT("PRN"), TEXT("AUX"), TEXT("CLOCK$"), TEXT("NUL"),
|
||||
TEXT("COM1"), TEXT("COM2"), TEXT("COM3"), TEXT("COM4"), TEXT("COM5"),
|
||||
TEXT("COM6"), TEXT("COM7"), TEXT("COM8"), TEXT("COM9"),
|
||||
TEXT("LPT1"), TEXT("LPT2"), TEXT("LPT3"), TEXT("LPT4"), TEXT("LPT5"),
|
||||
TEXT("LPT6"), TEXT("LPT7"), TEXT("LPT8"), TEXT("LPT9")
|
||||
};
|
||||
for (const TCHAR* ReservedName : ReservedNames)
|
||||
{
|
||||
if (Stem.Equals(ReservedName, ESearchCase::IgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool UDirectiveUtilStringFunctionLibrary::IsValidFileName(const FString& String)
|
||||
{
|
||||
return !String.IsEmpty() && FPaths::GetCleanFilename(String) == String && SanitizeFileName(String) == String;
|
||||
return !String.IsEmpty()
|
||||
&& String != TEXT(".")
|
||||
&& String != TEXT("..")
|
||||
&& FPaths::GetCleanFilename(String) == String
|
||||
&& SanitizeFileName(String) == String;
|
||||
}
|
||||
|
||||
FString UDirectiveUtilStringFunctionLibrary::SanitizeFileName(const FString& String, const FString& Replacement)
|
||||
{
|
||||
return FPaths::MakeValidFileName(String, Replacement.IsEmpty() ? TEXT('\0') : Replacement[0]);
|
||||
FString Result = FPaths::MakeValidFileName(String, Replacement.IsEmpty() ? TEXT('\0') : Replacement[0]);
|
||||
while (Result.EndsWith(TEXT(".")) || Result.EndsWith(TEXT(" ")))
|
||||
{
|
||||
Result.LeftChopInline(1);
|
||||
}
|
||||
if (IsReservedDeviceFileName(Result))
|
||||
{
|
||||
Result = FString(TEXT("_")) + Result;
|
||||
}
|
||||
return Result;
|
||||
}
|
||||
|
||||
int32 UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(const FString& Input, const TArray<FString>& Candidates, float& OutSimilarity, const bool bCaseSensitive)
|
||||
{
|
||||
OutSimilarity = 0.0f;
|
||||
int32 BestIndex = INDEX_NONE;
|
||||
const FString NormalizedInput = bCaseSensitive ? FString() : Input.ToLower();
|
||||
const FStringView InputView = bCaseSensitive ? FStringView(Input) : FStringView(NormalizedInput);
|
||||
TArray<int32> PreviousRow;
|
||||
TArray<int32> CurrentRow;
|
||||
|
||||
for (int32 Index = 0; Index < Candidates.Num(); ++Index)
|
||||
{
|
||||
const float Similarity = GetStringSimilarity(Input, Candidates[Index], bCaseSensitive);
|
||||
const FString NormalizedCandidate = bCaseSensitive ? FString() : Candidates[Index].ToLower();
|
||||
const FStringView CandidateView = bCaseSensitive
|
||||
? FStringView(Candidates[Index])
|
||||
: FStringView(NormalizedCandidate);
|
||||
const float Similarity = CalculateStringSimilarity(
|
||||
InputView,
|
||||
CandidateView,
|
||||
FMath::Max(Input.Len(), Candidates[Index].Len()),
|
||||
PreviousRow,
|
||||
CurrentRow);
|
||||
if (BestIndex == INDEX_NONE || Similarity > OutSimilarity)
|
||||
{
|
||||
BestIndex = Index;
|
||||
OutSimilarity = Similarity;
|
||||
if (Similarity == 1.0f)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return BestIndex;
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Tasks/DirectiveUtilAsyncActionBase.h"
|
||||
|
||||
#include "Engine/Engine.h"
|
||||
#include "Engine/World.h"
|
||||
|
||||
void UDirectiveUtilAsyncActionBase::RegisterWithGameInstance(const UObject* WorldContextObject)
|
||||
{
|
||||
UnbindWorldCleanup();
|
||||
Super::RegisterWithGameInstance(WorldContextObject);
|
||||
RegisteredWorld = WorldContextObject && GEngine
|
||||
? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull)
|
||||
: nullptr;
|
||||
if (RegisteredWorld.IsValid())
|
||||
{
|
||||
WorldCleanupHandle = FWorldDelegates::OnWorldCleanup.AddUObject(
|
||||
this,
|
||||
&UDirectiveUtilAsyncActionBase::HandleWorldCleanup);
|
||||
}
|
||||
}
|
||||
|
||||
void UDirectiveUtilAsyncActionBase::SetReadyToDestroy()
|
||||
{
|
||||
UnbindWorldCleanup();
|
||||
Super::SetReadyToDestroy();
|
||||
}
|
||||
|
||||
void UDirectiveUtilAsyncActionBase::BeginDestroy()
|
||||
{
|
||||
UnbindWorldCleanup();
|
||||
Super::BeginDestroy();
|
||||
}
|
||||
|
||||
void UDirectiveUtilAsyncActionBase::HandleWorldCleanup(
|
||||
UWorld* World,
|
||||
const bool,
|
||||
const bool)
|
||||
{
|
||||
if (World == RegisteredWorld.Get())
|
||||
{
|
||||
SetReadyToDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
void UDirectiveUtilAsyncActionBase::UnbindWorldCleanup()
|
||||
{
|
||||
if (WorldCleanupHandle.IsValid())
|
||||
{
|
||||
FWorldDelegates::OnWorldCleanup.Remove(WorldCleanupHandle);
|
||||
WorldCleanupHandle.Reset();
|
||||
}
|
||||
RegisteredWorld.Reset();
|
||||
}
|
||||
|
||||
void UDirectiveUtilCancellableAsyncAction::RegisterWithGameInstance(const UObject* WorldContextObject)
|
||||
{
|
||||
UnbindWorldCleanup();
|
||||
Super::RegisterWithGameInstance(WorldContextObject);
|
||||
RegisteredWorld = WorldContextObject && GEngine
|
||||
? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull)
|
||||
: nullptr;
|
||||
if (RegisteredWorld.IsValid())
|
||||
{
|
||||
WorldCleanupHandle = FWorldDelegates::OnWorldCleanup.AddUObject(
|
||||
this,
|
||||
&UDirectiveUtilCancellableAsyncAction::HandleWorldCleanup);
|
||||
}
|
||||
}
|
||||
|
||||
void UDirectiveUtilCancellableAsyncAction::SetReadyToDestroy()
|
||||
{
|
||||
UnbindWorldCleanup();
|
||||
Super::SetReadyToDestroy();
|
||||
}
|
||||
|
||||
void UDirectiveUtilCancellableAsyncAction::BeginDestroy()
|
||||
{
|
||||
UnbindWorldCleanup();
|
||||
Super::BeginDestroy();
|
||||
}
|
||||
|
||||
void UDirectiveUtilCancellableAsyncAction::HandleWorldCleanup(
|
||||
UWorld* World,
|
||||
const bool,
|
||||
const bool)
|
||||
{
|
||||
if (World == RegisteredWorld.Get())
|
||||
{
|
||||
Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
void UDirectiveUtilCancellableAsyncAction::UnbindWorldCleanup()
|
||||
{
|
||||
if (WorldCleanupHandle.IsValid())
|
||||
{
|
||||
FWorldDelegates::OnWorldCleanup.Remove(WorldCleanupHandle);
|
||||
WorldCleanupHandle.Reset();
|
||||
}
|
||||
RegisteredWorld.Reset();
|
||||
}
|
||||
@@ -52,7 +52,7 @@ void UDirectiveUtilTask_AsyncLoadAsset::Activate()
|
||||
|
||||
void UDirectiveUtilTask_AsyncLoadAsset::OnLoaded()
|
||||
{
|
||||
UObject* LoadedAsset = StreamableHandle.IsValid() ? StreamableHandle->GetLoadedAsset() : nullptr;
|
||||
UObject* LoadedAsset = SoftAsset.Get();
|
||||
if (LoadedAsset)
|
||||
{
|
||||
Completed.Broadcast(LoadedAsset);
|
||||
@@ -120,7 +120,7 @@ void UDirectiveUtilTask_AsyncLoadClass::Activate()
|
||||
|
||||
void UDirectiveUtilTask_AsyncLoadClass::OnLoaded()
|
||||
{
|
||||
UClass* LoadedClass = StreamableHandle.IsValid() ? Cast<UClass>(StreamableHandle->GetLoadedAsset()) : nullptr;
|
||||
UClass* LoadedClass = SoftClass.Get();
|
||||
if (LoadedClass)
|
||||
{
|
||||
Completed.Broadcast(LoadedClass);
|
||||
@@ -157,8 +157,6 @@ UDirectiveUtilTask_AsyncLoadAssets* UDirectiveUtilTask_AsyncLoadAssets::AsyncLoa
|
||||
|
||||
void UDirectiveUtilTask_AsyncLoadAssets::Activate()
|
||||
{
|
||||
// Unset references are filtered out of the request but keep their null slots in the output;
|
||||
// duplicates are requested once and resolved per slot.
|
||||
TArray<FSoftObjectPath> PathsToLoad;
|
||||
for (const TSoftObjectPtr<UObject>& SoftAsset : SoftAssets)
|
||||
{
|
||||
@@ -170,7 +168,6 @@ void UDirectiveUtilTask_AsyncLoadAssets::Activate()
|
||||
|
||||
if (PathsToLoad.Num() == 0)
|
||||
{
|
||||
// Nothing to request; an empty request list is an error path in the streamable manager.
|
||||
OnLoaded();
|
||||
return;
|
||||
}
|
||||
@@ -194,8 +191,6 @@ void UDirectiveUtilTask_AsyncLoadAssets::Activate()
|
||||
return;
|
||||
}
|
||||
|
||||
// Binding fails when the load already finished (all assets were in memory); complete directly,
|
||||
// with the guard keeping the broadcast exactly once.
|
||||
if (!StreamableHandle->BindUpdateDelegate(FStreamableUpdateDelegate::CreateUObject(this, &UDirectiveUtilTask_AsyncLoadAssets::OnUpdate)))
|
||||
{
|
||||
OnLoaded();
|
||||
|
||||
@@ -25,11 +25,28 @@ UDirectiveUtilTask_Delay* UDirectiveUtilTask_Delay::CancellableDelay(UObject* Wo
|
||||
|
||||
void UDirectiveUtilTask_Delay::EndTask()
|
||||
{
|
||||
if (UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull) : nullptr)
|
||||
Cancel();
|
||||
}
|
||||
|
||||
void UDirectiveUtilTask_Delay::Cancel()
|
||||
{
|
||||
bFinished = true;
|
||||
if (FTimerManager* TimerManager = GetTimerManager())
|
||||
{
|
||||
World->GetTimerManager().ClearTimer(TimerHandle);
|
||||
TimerManager->ClearTimer(TimerHandle);
|
||||
}
|
||||
SetReadyToDestroy();
|
||||
Completed.Clear();
|
||||
Super::Cancel();
|
||||
}
|
||||
|
||||
bool UDirectiveUtilTask_Delay::IsActive() const
|
||||
{
|
||||
return !bFinished && Super::IsActive();
|
||||
}
|
||||
|
||||
bool UDirectiveUtilTask_Delay::ShouldBroadcastDelegates() const
|
||||
{
|
||||
return !bFinished && Super::ShouldBroadcastDelegates();
|
||||
}
|
||||
|
||||
void UDirectiveUtilTask_Delay::Activate()
|
||||
@@ -40,11 +57,14 @@ void UDirectiveUtilTask_Delay::Activate()
|
||||
if (!World)
|
||||
{
|
||||
UE_LOG(LogDirectiveUtil, Warning, TEXT("Cancellable Delay failed to activate. World is null."));
|
||||
bFinished = true;
|
||||
SetReadyToDestroy();
|
||||
return;
|
||||
}
|
||||
|
||||
const float ClampedDuration = FMath::Max(Duration, KINDA_SMALL_NUMBER);
|
||||
const float ClampedDuration = FMath::IsFinite(Duration)
|
||||
? FMath::Max(Duration, KINDA_SMALL_NUMBER)
|
||||
: KINDA_SMALL_NUMBER;
|
||||
World->GetTimerManager().SetTimer(TimerHandle, this, &UDirectiveUtilTask_Delay::OnDelayComplete, ClampedDuration, false);
|
||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Cancellable Delay started for %f seconds."), ClampedDuration);
|
||||
}
|
||||
@@ -52,6 +72,10 @@ void UDirectiveUtilTask_Delay::Activate()
|
||||
void UDirectiveUtilTask_Delay::OnDelayComplete()
|
||||
{
|
||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Cancellable Delay completed."));
|
||||
Completed.Broadcast();
|
||||
if (ShouldBroadcastDelegates())
|
||||
{
|
||||
Completed.Broadcast();
|
||||
}
|
||||
bFinished = true;
|
||||
SetReadyToDestroy();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
|
||||
|
||||
#include "Tasks/DirectiveUtilTask_Flow.h"
|
||||
|
||||
#include "DirectiveUtilLogChannels.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "Engine/World.h"
|
||||
#include "TimerManager.h"
|
||||
|
||||
UDirectiveUtilTask_UpdateForDuration* UDirectiveUtilTask_UpdateForDuration::UpdateForDuration(
|
||||
UObject* WorldContextObject,
|
||||
const float Duration,
|
||||
const float UpdateInterval)
|
||||
{
|
||||
UDirectiveUtilTask_UpdateForDuration* Action = NewObject<UDirectiveUtilTask_UpdateForDuration>();
|
||||
Action->WorldContextObject = WorldContextObject;
|
||||
Action->Duration = Duration;
|
||||
Action->UpdateInterval = UpdateInterval;
|
||||
if (WorldContextObject)
|
||||
{
|
||||
Action->RegisterWithGameInstance(WorldContextObject);
|
||||
}
|
||||
return Action;
|
||||
}
|
||||
|
||||
void UDirectiveUtilTask_UpdateForDuration::Activate()
|
||||
{
|
||||
UWorld* World = WorldContextObject && GEngine
|
||||
? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull)
|
||||
: nullptr;
|
||||
if (!World)
|
||||
{
|
||||
UE_LOG(LogDirectiveUtil, Warning, TEXT("Update for Duration failed to activate. World is null."));
|
||||
bFinished = true;
|
||||
SetReadyToDestroy();
|
||||
return;
|
||||
}
|
||||
|
||||
FTimerManager& TimerManager = World->GetTimerManager();
|
||||
if (!FMath::IsFinite(Duration) || Duration <= 0.0f)
|
||||
{
|
||||
CompletionTimerHandle = TimerManager.SetTimerForNextTick(this, &UDirectiveUtilTask_UpdateForDuration::OnComplete);
|
||||
return;
|
||||
}
|
||||
|
||||
TimerManager.SetTimer(
|
||||
CompletionTimerHandle,
|
||||
this,
|
||||
&UDirectiveUtilTask_UpdateForDuration::OnComplete,
|
||||
Duration,
|
||||
false);
|
||||
|
||||
if (!FMath::IsFinite(UpdateInterval) || UpdateInterval <= 0.0f)
|
||||
{
|
||||
UpdateTimerHandle = TimerManager.SetTimerForNextTick(this, &UDirectiveUtilTask_UpdateForDuration::OnUpdate);
|
||||
return;
|
||||
}
|
||||
|
||||
FTimerManagerTimerParameters UpdateParameters;
|
||||
UpdateParameters.bLoop = true;
|
||||
UpdateParameters.bMaxOncePerFrame = true;
|
||||
UpdateParameters.FirstDelay = UpdateInterval;
|
||||
TimerManager.SetTimer(
|
||||
UpdateTimerHandle,
|
||||
this,
|
||||
&UDirectiveUtilTask_UpdateForDuration::OnUpdate,
|
||||
UpdateInterval,
|
||||
UpdateParameters);
|
||||
}
|
||||
|
||||
void UDirectiveUtilTask_UpdateForDuration::Cancel()
|
||||
{
|
||||
bFinished = true;
|
||||
ClearTimers();
|
||||
Updated.Clear();
|
||||
Completed.Clear();
|
||||
Super::Cancel();
|
||||
}
|
||||
|
||||
bool UDirectiveUtilTask_UpdateForDuration::IsActive() const
|
||||
{
|
||||
return !bFinished && Super::IsActive();
|
||||
}
|
||||
|
||||
bool UDirectiveUtilTask_UpdateForDuration::ShouldBroadcastDelegates() const
|
||||
{
|
||||
return !bFinished && Super::ShouldBroadcastDelegates();
|
||||
}
|
||||
|
||||
void UDirectiveUtilTask_UpdateForDuration::OnUpdate()
|
||||
{
|
||||
if (!ShouldBroadcastDelegates())
|
||||
{
|
||||
ClearTimers();
|
||||
return;
|
||||
}
|
||||
|
||||
FTimerManager* TimerManager = GetTimerManager();
|
||||
if (!TimerManager)
|
||||
{
|
||||
Cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
const float RemainingTime = TimerManager->GetTimerRemaining(CompletionTimerHandle);
|
||||
const float ElapsedTime = RemainingTime >= 0.0f
|
||||
? FMath::Clamp(Duration - RemainingTime, 0.0f, Duration)
|
||||
: Duration;
|
||||
BroadcastUpdate(ElapsedTime, ElapsedTime / Duration);
|
||||
if (ShouldBroadcastDelegates() && (!FMath::IsFinite(UpdateInterval) || UpdateInterval <= 0.0f))
|
||||
{
|
||||
UpdateTimerHandle = TimerManager->SetTimerForNextTick(this, &UDirectiveUtilTask_UpdateForDuration::OnUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
void UDirectiveUtilTask_UpdateForDuration::OnComplete()
|
||||
{
|
||||
if (!ShouldBroadcastDelegates())
|
||||
{
|
||||
ClearTimers();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!bHasUpdated || LastElapsedTime < Duration)
|
||||
{
|
||||
const float FinalElapsedTime = FMath::IsFinite(Duration) && Duration > 0.0f ? Duration : 0.0f;
|
||||
BroadcastUpdate(FinalElapsedTime, 1.0f);
|
||||
}
|
||||
if (!ShouldBroadcastDelegates())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClearTimers();
|
||||
Completed.Broadcast();
|
||||
bFinished = true;
|
||||
SetReadyToDestroy();
|
||||
}
|
||||
|
||||
void UDirectiveUtilTask_UpdateForDuration::BroadcastUpdate(const float ElapsedTime, const float Alpha)
|
||||
{
|
||||
const float DeltaTime = bHasUpdated ? FMath::Max(ElapsedTime - LastElapsedTime, 0.0f) : ElapsedTime;
|
||||
LastElapsedTime = ElapsedTime;
|
||||
bHasUpdated = true;
|
||||
Updated.Broadcast(ElapsedTime, DeltaTime, Alpha);
|
||||
}
|
||||
|
||||
void UDirectiveUtilTask_UpdateForDuration::ClearTimers()
|
||||
{
|
||||
if (FTimerManager* TimerManager = GetTimerManager())
|
||||
{
|
||||
TimerManager->ClearTimer(UpdateTimerHandle);
|
||||
TimerManager->ClearTimer(CompletionTimerHandle);
|
||||
}
|
||||
}
|
||||
|
||||
UDirectiveUtilTask_RepeatWithInterval* UDirectiveUtilTask_RepeatWithInterval::RepeatWithInterval(
|
||||
UObject* WorldContextObject,
|
||||
const int32 Count,
|
||||
const float Interval,
|
||||
const float InitialDelay)
|
||||
{
|
||||
UDirectiveUtilTask_RepeatWithInterval* Action = NewObject<UDirectiveUtilTask_RepeatWithInterval>();
|
||||
Action->WorldContextObject = WorldContextObject;
|
||||
Action->Count = Count;
|
||||
Action->Interval = Interval;
|
||||
Action->InitialDelay = InitialDelay;
|
||||
if (WorldContextObject)
|
||||
{
|
||||
Action->RegisterWithGameInstance(WorldContextObject);
|
||||
}
|
||||
return Action;
|
||||
}
|
||||
|
||||
void UDirectiveUtilTask_RepeatWithInterval::Activate()
|
||||
{
|
||||
UWorld* World = WorldContextObject && GEngine
|
||||
? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull)
|
||||
: nullptr;
|
||||
if (!World)
|
||||
{
|
||||
UE_LOG(LogDirectiveUtil, Warning, TEXT("Repeat with Interval failed to activate. World is null."));
|
||||
bFinished = true;
|
||||
SetReadyToDestroy();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Count == 0 || Count < -1)
|
||||
{
|
||||
TimerHandle = World->GetTimerManager().SetTimerForNextTick(this, &UDirectiveUtilTask_RepeatWithInterval::Complete);
|
||||
return;
|
||||
}
|
||||
|
||||
const float SafeInitialDelay = FMath::IsFinite(InitialDelay) && InitialDelay > 0.0f
|
||||
? InitialDelay
|
||||
: 0.0f;
|
||||
Schedule(SafeInitialDelay);
|
||||
}
|
||||
|
||||
void UDirectiveUtilTask_RepeatWithInterval::Cancel()
|
||||
{
|
||||
bFinished = true;
|
||||
ClearTimer();
|
||||
Iteration.Clear();
|
||||
Completed.Clear();
|
||||
Super::Cancel();
|
||||
}
|
||||
|
||||
bool UDirectiveUtilTask_RepeatWithInterval::IsActive() const
|
||||
{
|
||||
return !bFinished && Super::IsActive();
|
||||
}
|
||||
|
||||
bool UDirectiveUtilTask_RepeatWithInterval::ShouldBroadcastDelegates() const
|
||||
{
|
||||
return !bFinished && Super::ShouldBroadcastDelegates();
|
||||
}
|
||||
|
||||
void UDirectiveUtilTask_RepeatWithInterval::Schedule(const float Delay)
|
||||
{
|
||||
FTimerManager* TimerManager = GetTimerManager();
|
||||
if (!TimerManager)
|
||||
{
|
||||
Cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Delay > 0.0f)
|
||||
{
|
||||
TimerManager->SetTimer(TimerHandle, this, &UDirectiveUtilTask_RepeatWithInterval::OnIteration, Delay, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
TimerHandle = TimerManager->SetTimerForNextTick(this, &UDirectiveUtilTask_RepeatWithInterval::OnIteration);
|
||||
}
|
||||
}
|
||||
|
||||
void UDirectiveUtilTask_RepeatWithInterval::OnIteration()
|
||||
{
|
||||
if (!ShouldBroadcastDelegates())
|
||||
{
|
||||
ClearTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
const int32 CurrentIndex = NextIndex;
|
||||
NextIndex = NextIndex == MAX_int32 ? 0 : NextIndex + 1;
|
||||
const int32 Remaining = Count == -1 ? -1 : Count - NextIndex;
|
||||
Iteration.Broadcast(CurrentIndex, Remaining);
|
||||
if (!ShouldBroadcastDelegates())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Count != -1 && NextIndex >= Count)
|
||||
{
|
||||
Complete();
|
||||
return;
|
||||
}
|
||||
|
||||
const float SafeInterval = FMath::IsFinite(Interval) && Interval > 0.0f
|
||||
? Interval
|
||||
: 0.0f;
|
||||
Schedule(SafeInterval);
|
||||
}
|
||||
|
||||
void UDirectiveUtilTask_RepeatWithInterval::Complete()
|
||||
{
|
||||
if (!ShouldBroadcastDelegates())
|
||||
{
|
||||
ClearTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
ClearTimer();
|
||||
Completed.Broadcast();
|
||||
bFinished = true;
|
||||
SetReadyToDestroy();
|
||||
}
|
||||
|
||||
void UDirectiveUtilTask_RepeatWithInterval::ClearTimer()
|
||||
{
|
||||
if (FTimerManager* TimerManager = GetTimerManager())
|
||||
{
|
||||
TimerManager->ClearTimer(TimerHandle);
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,22 @@
|
||||
#include "Blueprint/AIBlueprintHelperLibrary.h"
|
||||
#include "Engine/World.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
#include "NavigationSystem.h"
|
||||
#include "Navigation/PathFollowingComponent.h"
|
||||
#include "TimerManager.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
float GetNonNegativeFiniteValue(const float Value)
|
||||
{
|
||||
return FMath::IsFinite(Value) ? FMath::Max(Value, 0.0f) : 0.0f;
|
||||
}
|
||||
|
||||
bool IsWithinAcceptanceRadius(const FVector& CurrentLocation, const FVector& TargetLocation, const float AcceptanceRadius)
|
||||
{
|
||||
return FVector::DistSquared2D(CurrentLocation, TargetLocation) <= FMath::Square(AcceptanceRadius);
|
||||
}
|
||||
}
|
||||
|
||||
UDirectiveUtilTask_MoveToLocation* UDirectiveUtilTask_MoveToLocation::MoveToLocation(
|
||||
UObject* WorldContextObject,
|
||||
@@ -22,9 +35,9 @@ UDirectiveUtilTask_MoveToLocation* UDirectiveUtilTask_MoveToLocation::MoveToLoca
|
||||
UDirectiveUtilTask_MoveToLocation* Action = NewObject<UDirectiveUtilTask_MoveToLocation>();
|
||||
Action->Controller = Controller;
|
||||
Action->Destination = Destination;
|
||||
Action->AcceptanceRadius = AcceptanceRadius;
|
||||
Action->AcceptanceRadius = GetNonNegativeFiniteValue(AcceptanceRadius);
|
||||
Action->bDebugLineTrace = bDebugLineTrace;
|
||||
Action->StuckThreshold = StuckThreshold;
|
||||
Action->StuckThreshold = GetNonNegativeFiniteValue(StuckThreshold);
|
||||
Action->bCheckStuckMovement = bCheckStuckMovement;
|
||||
|
||||
if (WorldContextObject)
|
||||
@@ -38,27 +51,45 @@ UDirectiveUtilTask_MoveToLocation* UDirectiveUtilTask_MoveToLocation::MoveToLoca
|
||||
|
||||
void UDirectiveUtilTask_MoveToLocation::EndTask()
|
||||
{
|
||||
if (IsValid(Controller))
|
||||
{
|
||||
Controller->StopMovement();
|
||||
}
|
||||
ExecuteCompleted(false);
|
||||
}
|
||||
|
||||
void UDirectiveUtilTask_MoveToLocation::Activate()
|
||||
{
|
||||
if(!Controller || !Controller->GetPawn())
|
||||
if (bHasCompleted)
|
||||
{
|
||||
ExecuteCompleted(false);
|
||||
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller or pawn has been destroyed while moving to location. Aborting."));
|
||||
return;
|
||||
}
|
||||
|
||||
StartLocation = Controller->GetPawn()->GetActorLocation();
|
||||
APawn* Pawn = IsValid(Controller) ? Controller->GetPawn() : nullptr;
|
||||
UWorld* World = IsValid(Pawn) ? Controller->GetWorld() : nullptr;
|
||||
if (!World)
|
||||
{
|
||||
ExecuteCompleted(false);
|
||||
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller, pawn, or world is unavailable while moving to location. Aborting."));
|
||||
return;
|
||||
}
|
||||
if (!FNavigationSystem::GetCurrent<UNavigationSystemV1>(World))
|
||||
{
|
||||
ExecuteCompleted(false);
|
||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Navigation is unavailable while moving to location."));
|
||||
return;
|
||||
}
|
||||
|
||||
TimerWorld = World;
|
||||
StartLocation = Pawn->GetActorLocation();
|
||||
LastCheckedLocation = StartLocation;
|
||||
CurrentLocation = StartLocation;
|
||||
|
||||
Controller->GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &UDirectiveUtilTask_MoveToLocation::CheckMoveToLocation, 0.1f, true);
|
||||
World->GetTimerManager().SetTimer(TimerHandle, this, &UDirectiveUtilTask_MoveToLocation::CheckMoveToLocation, 0.1f, true);
|
||||
|
||||
if (bCheckStuckMovement)
|
||||
{
|
||||
Controller->GetWorld()->GetTimerManager().SetTimer(StuckTimerHandle, this, &UDirectiveUtilTask_MoveToLocation::CheckStuckMovement, 3.f, true);
|
||||
World->GetTimerManager().SetTimer(StuckTimerHandle, this, &UDirectiveUtilTask_MoveToLocation::CheckStuckMovement, 3.f, true);
|
||||
}
|
||||
|
||||
UAIBlueprintHelperLibrary::SimpleMoveToLocation(Controller, Destination);
|
||||
@@ -67,7 +98,7 @@ void UDirectiveUtilTask_MoveToLocation::Activate()
|
||||
if (bDebugLineTrace)
|
||||
{
|
||||
DrawDebugLine(
|
||||
Controller->GetWorld(),
|
||||
World,
|
||||
Destination + FVector(0, 0, 100),
|
||||
Destination,
|
||||
FColor::Green,
|
||||
@@ -81,18 +112,18 @@ void UDirectiveUtilTask_MoveToLocation::Activate()
|
||||
|
||||
void UDirectiveUtilTask_MoveToLocation::CheckMoveToLocation()
|
||||
{
|
||||
|
||||
if(!Controller || !Controller->GetPawn())
|
||||
APawn* Pawn = IsValid(Controller) ? Controller->GetPawn() : nullptr;
|
||||
if (!IsValid(Pawn) || !TimerWorld.IsValid())
|
||||
{
|
||||
ExecuteCompleted(false);
|
||||
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller or pawn has been destroyed while moving to location. Aborting."));
|
||||
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller, pawn, or world is unavailable while moving to location. Aborting."));
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentLocation = Controller->GetPawn()->GetActorLocation();
|
||||
CurrentLocation = Pawn->GetActorLocation();
|
||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Controller is moving to location (%s). Current distance: %f."), *Destination.ToString(), FVector::Dist(CurrentLocation, Destination));
|
||||
|
||||
if (FVector::Dist(CurrentLocation, Destination) < AcceptanceRadius)
|
||||
if (IsWithinAcceptanceRadius(CurrentLocation, Destination, AcceptanceRadius))
|
||||
{
|
||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Controller has moved to location."));
|
||||
ExecuteCompleted(true);
|
||||
@@ -103,16 +134,17 @@ void UDirectiveUtilTask_MoveToLocation::CheckMoveToLocation()
|
||||
if (!PathFollowing || PathFollowing->GetStatus() == EPathFollowingStatus::Idle)
|
||||
{
|
||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Path following has stopped. Completing move to location."));
|
||||
ExecuteCompleted(FVector::Dist(CurrentLocation, Destination) < AcceptanceRadius);
|
||||
ExecuteCompleted(IsWithinAcceptanceRadius(CurrentLocation, Destination, AcceptanceRadius));
|
||||
}
|
||||
}
|
||||
|
||||
void UDirectiveUtilTask_MoveToLocation::CheckStuckMovement()
|
||||
{
|
||||
if(!Controller || !Controller->GetPawn())
|
||||
const APawn* Pawn = IsValid(Controller) ? Controller->GetPawn() : nullptr;
|
||||
if (!IsValid(Pawn) || !TimerWorld.IsValid())
|
||||
{
|
||||
ExecuteCompleted(false);
|
||||
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller or pawn has been destroyed while moving to location. Aborting."));
|
||||
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller, pawn, or world is unavailable while moving to location. Aborting."));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -133,13 +165,14 @@ void UDirectiveUtilTask_MoveToLocation::ExecuteCompleted(const bool bSuccess)
|
||||
}
|
||||
bHasCompleted = true;
|
||||
|
||||
UE_LOG(LogDirectiveUtil, Log, TEXT("Movement to location completed. Success: %s."), bSuccess ? TEXT("true") : TEXT("false"));
|
||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Movement to location completed. Success: %s."), bSuccess ? TEXT("true") : TEXT("false"));
|
||||
|
||||
if (Controller)
|
||||
if (UWorld* World = TimerWorld.Get())
|
||||
{
|
||||
Controller->GetWorld()->GetTimerManager().ClearTimer(TimerHandle);
|
||||
Controller->GetWorld()->GetTimerManager().ClearTimer(StuckTimerHandle);
|
||||
World->GetTimerManager().ClearTimer(TimerHandle);
|
||||
World->GetTimerManager().ClearTimer(StuckTimerHandle);
|
||||
}
|
||||
TimerWorld.Reset();
|
||||
|
||||
Completed.Broadcast(bSuccess);
|
||||
|
||||
@@ -160,8 +193,8 @@ UDirectiveUtilTask_MoveToActor* UDirectiveUtilTask_MoveToActor::MoveToActor(
|
||||
UDirectiveUtilTask_MoveToActor* Action = NewObject<UDirectiveUtilTask_MoveToActor>();
|
||||
Action->Controller = Controller;
|
||||
Action->Goal = Goal;
|
||||
Action->AcceptanceRadius = AcceptanceRadius;
|
||||
Action->StuckThreshold = StuckThreshold;
|
||||
Action->AcceptanceRadius = GetNonNegativeFiniteValue(AcceptanceRadius);
|
||||
Action->StuckThreshold = GetNonNegativeFiniteValue(StuckThreshold);
|
||||
Action->bCheckStuckMovement = bCheckStuckMovement;
|
||||
|
||||
if (WorldContextObject)
|
||||
@@ -174,27 +207,45 @@ UDirectiveUtilTask_MoveToActor* UDirectiveUtilTask_MoveToActor::MoveToActor(
|
||||
|
||||
void UDirectiveUtilTask_MoveToActor::EndTask()
|
||||
{
|
||||
if (IsValid(Controller))
|
||||
{
|
||||
Controller->StopMovement();
|
||||
}
|
||||
ExecuteCompleted(false);
|
||||
}
|
||||
|
||||
void UDirectiveUtilTask_MoveToActor::Activate()
|
||||
{
|
||||
if(!Controller || !Controller->GetPawn() || !IsValid(Goal))
|
||||
if (bHasCompleted)
|
||||
{
|
||||
ExecuteCompleted(false);
|
||||
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller, pawn, or goal has been destroyed while moving to actor. Aborting."));
|
||||
return;
|
||||
}
|
||||
|
||||
StartLocation = Controller->GetPawn()->GetActorLocation();
|
||||
APawn* Pawn = IsValid(Controller) ? Controller->GetPawn() : nullptr;
|
||||
UWorld* World = IsValid(Pawn) ? Controller->GetWorld() : nullptr;
|
||||
if (!World || !IsValid(Goal))
|
||||
{
|
||||
ExecuteCompleted(false);
|
||||
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller, pawn, goal, or world is unavailable while moving to actor. Aborting."));
|
||||
return;
|
||||
}
|
||||
if (!FNavigationSystem::GetCurrent<UNavigationSystemV1>(World))
|
||||
{
|
||||
ExecuteCompleted(false);
|
||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Navigation is unavailable while moving to actor."));
|
||||
return;
|
||||
}
|
||||
|
||||
TimerWorld = World;
|
||||
StartLocation = Pawn->GetActorLocation();
|
||||
LastCheckedLocation = StartLocation;
|
||||
CurrentLocation = StartLocation;
|
||||
|
||||
Controller->GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &UDirectiveUtilTask_MoveToActor::CheckMoveToActor, 0.1f, true);
|
||||
World->GetTimerManager().SetTimer(TimerHandle, this, &UDirectiveUtilTask_MoveToActor::CheckMoveToActor, 0.1f, true);
|
||||
|
||||
if (bCheckStuckMovement)
|
||||
{
|
||||
Controller->GetWorld()->GetTimerManager().SetTimer(StuckTimerHandle, this, &UDirectiveUtilTask_MoveToActor::CheckStuckMovement, 3.f, true);
|
||||
World->GetTimerManager().SetTimer(StuckTimerHandle, this, &UDirectiveUtilTask_MoveToActor::CheckStuckMovement, 3.f, true);
|
||||
}
|
||||
|
||||
UAIBlueprintHelperLibrary::SimpleMoveToActor(Controller, Goal);
|
||||
@@ -203,10 +254,11 @@ void UDirectiveUtilTask_MoveToActor::Activate()
|
||||
|
||||
void UDirectiveUtilTask_MoveToActor::CheckMoveToActor()
|
||||
{
|
||||
if(!Controller || !Controller->GetPawn())
|
||||
APawn* Pawn = IsValid(Controller) ? Controller->GetPawn() : nullptr;
|
||||
if (!IsValid(Pawn) || !TimerWorld.IsValid())
|
||||
{
|
||||
ExecuteCompleted(false);
|
||||
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller or pawn has been destroyed while moving to actor. Aborting."));
|
||||
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller, pawn, or world is unavailable while moving to actor. Aborting."));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -219,10 +271,10 @@ void UDirectiveUtilTask_MoveToActor::CheckMoveToActor()
|
||||
|
||||
// The goal can move, so its location is re-read every poll.
|
||||
const FVector GoalLocation = Goal->GetActorLocation();
|
||||
CurrentLocation = Controller->GetPawn()->GetActorLocation();
|
||||
CurrentLocation = Pawn->GetActorLocation();
|
||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Controller is moving to actor (%s). Current distance: %f."), *GetNameSafe(Goal), FVector::Dist(CurrentLocation, GoalLocation));
|
||||
|
||||
if (FVector::Dist(CurrentLocation, GoalLocation) < AcceptanceRadius)
|
||||
if (IsWithinAcceptanceRadius(CurrentLocation, GoalLocation, AcceptanceRadius))
|
||||
{
|
||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Controller has moved to actor."));
|
||||
ExecuteCompleted(true);
|
||||
@@ -233,16 +285,17 @@ void UDirectiveUtilTask_MoveToActor::CheckMoveToActor()
|
||||
if (!PathFollowing || PathFollowing->GetStatus() == EPathFollowingStatus::Idle)
|
||||
{
|
||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Path following has stopped. Completing move to actor."));
|
||||
ExecuteCompleted(FVector::Dist(CurrentLocation, GoalLocation) < AcceptanceRadius);
|
||||
ExecuteCompleted(IsWithinAcceptanceRadius(CurrentLocation, GoalLocation, AcceptanceRadius));
|
||||
}
|
||||
}
|
||||
|
||||
void UDirectiveUtilTask_MoveToActor::CheckStuckMovement()
|
||||
{
|
||||
if(!Controller || !Controller->GetPawn())
|
||||
const APawn* Pawn = IsValid(Controller) ? Controller->GetPawn() : nullptr;
|
||||
if (!IsValid(Pawn) || !TimerWorld.IsValid())
|
||||
{
|
||||
ExecuteCompleted(false);
|
||||
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller or pawn has been destroyed while moving to actor. Aborting."));
|
||||
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller, pawn, or world is unavailable while moving to actor. Aborting."));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -263,13 +316,14 @@ void UDirectiveUtilTask_MoveToActor::ExecuteCompleted(const bool bSuccess)
|
||||
}
|
||||
bHasCompleted = true;
|
||||
|
||||
UE_LOG(LogDirectiveUtil, Log, TEXT("Movement to actor completed. Success: %s."), bSuccess ? TEXT("true") : TEXT("false"));
|
||||
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Movement to actor completed. Success: %s."), bSuccess ? TEXT("true") : TEXT("false"));
|
||||
|
||||
if (Controller)
|
||||
if (UWorld* World = TimerWorld.Get())
|
||||
{
|
||||
Controller->GetWorld()->GetTimerManager().ClearTimer(TimerHandle);
|
||||
Controller->GetWorld()->GetTimerManager().ClearTimer(StuckTimerHandle);
|
||||
World->GetTimerManager().ClearTimer(TimerHandle);
|
||||
World->GetTimerManager().ClearTimer(StuckTimerHandle);
|
||||
}
|
||||
TimerWorld.Reset();
|
||||
|
||||
Completed.Broadcast(bSuccess);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user