Import helper plugin

This commit is contained in:
2026-07-15 19:17:47 +02:00
parent fd992725a2
commit 3c084d9669
72 changed files with 11853 additions and 1 deletions

View File

@@ -0,0 +1,4 @@
#include "DirectiveUtilLogChannels.h"
DEFINE_LOG_CATEGORY(LogDirectiveUtil);
DEFINE_LOG_CATEGORY(LogDirectiveUtilEditor);

View File

@@ -0,0 +1,13 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "DirectiveUtilitiesRuntime.h"
void FDirectiveUtilitiesRuntimeModule::StartupModule()
{
}
void FDirectiveUtilitiesRuntimeModule::ShutdownModule()
{
}
IMPLEMENT_MODULE(FDirectiveUtilitiesRuntimeModule, DirectiveUtilitiesRuntime)

View File

@@ -0,0 +1,565 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilArrayFunctionLibrary.h"
int32 UDirectiveUtilArrayFunctionLibrary::Array_NextIndex(const TArray<int32>& TargetArray, const int32 Index, const bool bLoop)
{
checkNoEntry();
return 0;
}
int32 UDirectiveUtilArrayFunctionLibrary::Array_PreviousIndex(
const TArray<int32>& TargetArray,
const int32 Index,
const bool bLoop)
{
checkNoEntry();
return 0;
}
int32 UDirectiveUtilArrayFunctionLibrary::GenericArray_NextIndex(
const void* TargetArray,
const FArrayProperty* ArrayProperty,
const int32 Index,
const bool bLoop)
{
if (!TargetArray || !ArrayProperty)
{
return INDEX_NONE;
}
const FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
const int32 NextIndex = Index + 1;
if(ArrayHelper.Num() == 0)
{
return INDEX_NONE;
}
if(NextIndex < 0)
{
return 0;
}
if (NextIndex <= ArrayHelper.Num() - 1)
{
return NextIndex;
}
if (bLoop)
{
return 0;
}
return ArrayHelper.Num() - 1;
}
void UDirectiveUtilArrayFunctionLibrary::Array_RemoveDuplicates(const TArray<int32>& TargetArray)
{
checkNoEntry();
}
void UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveDuplicates(
void* TargetArray,
const FArrayProperty* ArrayProperty)
{
if (!TargetArray || !ArrayProperty)
{
return;
}
FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
const FProperty* InnerProp = ArrayProperty->Inner;
for (int32 OuterIndex = ArrayHelper.Num() - 1; OuterIndex > 0; --OuterIndex)
{
for (int32 InnerIndex = 0; InnerIndex < OuterIndex; ++InnerIndex)
{
if (InnerProp->Identical(ArrayHelper.GetElementPtr(OuterIndex), ArrayHelper.GetElementPtr(InnerIndex)))
{
ArrayHelper.RemoveValues(OuterIndex);
break;
}
}
}
}
int32 UDirectiveUtilArrayFunctionLibrary::GenericArray_PreviousIndex(
const void* TargetArray,
const FArrayProperty* ArrayProperty,
const int32 Index,
const bool bLoop)
{
if (!TargetArray || !ArrayProperty)
{
return INDEX_NONE;
}
const FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
const int32 PreviousIndex = Index - 1;
if(ArrayHelper.Num() == 0)
{
return INDEX_NONE;
}
if(PreviousIndex > ArrayHelper.Num() - 1)
{
return ArrayHelper.Num() - 1;
}
if (PreviousIndex >= 0)
{
return PreviousIndex;
}
if (bLoop)
{
return ArrayHelper.Num() - 1;
}
return 0;
}
bool UDirectiveUtilArrayFunctionLibrary::Array_GetValidFirstItemCopy(const TArray<int32>& TargetArray, int32& OutItem)
{
checkNoEntry();
return false;
}
bool UDirectiveUtilArrayFunctionLibrary::Array_GetValidLastItemCopy(const TArray<int32>& TargetArray, int32& OutItem)
{
checkNoEntry();
return false;
}
bool UDirectiveUtilArrayFunctionLibrary::Array_GetValidItemFromIndexCopy(const TArray<int32>& TargetArray, const int32 Index, int32& OutItem)
{
checkNoEntry();
return false;
}
bool UDirectiveUtilArrayFunctionLibrary::Array_GetRandomItem(const TArray<int32>& TargetArray, int32& OutItem, int32& OutIndex)
{
checkNoEntry();
return false;
}
void UDirectiveUtilArrayFunctionLibrary::Array_LastValue(const TArray<int32>& TargetArray, int32& OutItem)
{
checkNoEntry();
}
bool UDirectiveUtilArrayFunctionLibrary::Array_Pop(const TArray<int32>& TargetArray, int32& OutItem)
{
checkNoEntry();
return false;
}
bool UDirectiveUtilArrayFunctionLibrary::Array_PopFirst(const TArray<int32>& TargetArray, int32& OutItem)
{
checkNoEntry();
return false;
}
bool UDirectiveUtilArrayFunctionLibrary::Array_RemoveAtSwap(const TArray<int32>& TargetArray, const int32 Index)
{
checkNoEntry();
return false;
}
bool UDirectiveUtilArrayFunctionLibrary::GenericArray_GetItemAtIndex(
const void* TargetArray,
const FArrayProperty* ArrayProperty,
const int32 Index,
void* OutItemPtr)
{
if (!TargetArray || !ArrayProperty)
{
return false;
}
FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
const FProperty* InnerProp = ArrayProperty->Inner;
if (!ArrayHelper.IsValidIndex(Index))
{
if (OutItemPtr)
{
InnerProp->ClearValue(OutItemPtr);
}
return false;
}
if (OutItemPtr)
{
InnerProp->CopyCompleteValueFromScriptVM(OutItemPtr, ArrayHelper.GetRawPtr(Index));
}
return true;
}
bool UDirectiveUtilArrayFunctionLibrary::GenericArray_GetFirstItem(
const void* TargetArray,
const FArrayProperty* ArrayProperty,
void* OutItemPtr)
{
return GenericArray_GetItemAtIndex(TargetArray, ArrayProperty, 0, OutItemPtr);
}
bool UDirectiveUtilArrayFunctionLibrary::GenericArray_GetLastItem(
const void* TargetArray,
const FArrayProperty* ArrayProperty,
void* OutItemPtr)
{
if (!TargetArray || !ArrayProperty)
{
return false;
}
const FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
return GenericArray_GetItemAtIndex(TargetArray, ArrayProperty, ArrayHelper.Num() - 1, OutItemPtr);
}
bool UDirectiveUtilArrayFunctionLibrary::GenericArray_GetRandomItem(
const void* TargetArray,
const FArrayProperty* ArrayProperty,
void* OutItemPtr,
int32* OutIndex)
{
if (OutIndex)
{
*OutIndex = INDEX_NONE;
}
if (!TargetArray || !ArrayProperty)
{
return false;
}
const FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
const int32 Num = ArrayHelper.Num();
if (Num <= 0)
{
if (OutItemPtr)
{
ArrayProperty->Inner->ClearValue(OutItemPtr);
}
return false;
}
const int32 Index = FMath::RandRange(0, Num - 1);
if (OutIndex)
{
*OutIndex = Index;
}
return GenericArray_GetItemAtIndex(TargetArray, ArrayProperty, Index, OutItemPtr);
}
bool UDirectiveUtilArrayFunctionLibrary::GenericArray_Pop(
void* TargetArray,
const FArrayProperty* ArrayProperty,
void* OutItemPtr)
{
if (!TargetArray || !ArrayProperty)
{
return false;
}
FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
const FProperty* InnerProp = ArrayProperty->Inner;
const int32 LastIndex = ArrayHelper.Num() - 1;
if (LastIndex < 0)
{
if (OutItemPtr)
{
InnerProp->ClearValue(OutItemPtr);
}
return false;
}
if (OutItemPtr)
{
InnerProp->CopyCompleteValueFromScriptVM(OutItemPtr, ArrayHelper.GetRawPtr(LastIndex));
}
ArrayHelper.RemoveValues(LastIndex, 1);
return true;
}
bool UDirectiveUtilArrayFunctionLibrary::GenericArray_PopFirst(
void* TargetArray,
const FArrayProperty* ArrayProperty,
void* OutItemPtr)
{
if (!TargetArray || !ArrayProperty)
{
return false;
}
FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
const FProperty* InnerProp = ArrayProperty->Inner;
if (ArrayHelper.Num() <= 0)
{
if (OutItemPtr)
{
InnerProp->ClearValue(OutItemPtr);
}
return false;
}
if (OutItemPtr)
{
InnerProp->CopyCompleteValueFromScriptVM(OutItemPtr, ArrayHelper.GetRawPtr(0));
}
ArrayHelper.RemoveValues(0, 1);
return true;
}
bool UDirectiveUtilArrayFunctionLibrary::GenericArray_RemoveAtSwap(
void* TargetArray,
const FArrayProperty* ArrayProperty,
const int32 Index)
{
if (!TargetArray || !ArrayProperty)
{
return false;
}
FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
if (!ArrayHelper.IsValidIndex(Index))
{
return false;
}
const int32 LastIndex = ArrayHelper.Num() - 1;
if (Index != LastIndex)
{
ArrayHelper.SwapValues(Index, LastIndex);
}
ArrayHelper.RemoveValues(LastIndex, 1);
return true;
}
void UDirectiveUtilArrayFunctionLibrary::Array_Slice(const TArray<int32>& TargetArray, const int32 StartIndex, const int32 Count, TArray<int32>& OutArray)
{
checkNoEntry();
}
void UDirectiveUtilArrayFunctionLibrary::Array_Rotate(const TArray<int32>& TargetArray, const int32 Shift)
{
checkNoEntry();
}
void UDirectiveUtilArrayFunctionLibrary::Array_GetDistinct(const TArray<int32>& TargetArray, TArray<int32>& OutArray)
{
checkNoEntry();
}
int32 UDirectiveUtilArrayFunctionLibrary::Array_CountOccurrences(const TArray<int32>& TargetArray, const int32& ItemToCount)
{
checkNoEntry();
return 0;
}
bool UDirectiveUtilArrayFunctionLibrary::Array_GetMostCommon(const TArray<int32>& TargetArray, int32& OutItem, int32& OutCount)
{
checkNoEntry();
return false;
}
void UDirectiveUtilArrayFunctionLibrary::GenericArray_Slice(
const void* TargetArray,
const FArrayProperty* TargetArrayProperty,
const int32 StartIndex,
const int32 Count,
void* OutArray,
const FArrayProperty* OutArrayProperty)
{
if (!TargetArray || !OutArray || !TargetArrayProperty || !OutArrayProperty)
{
return;
}
FScriptArrayHelper SourceHelper(TargetArrayProperty, TargetArray);
FScriptArrayHelper OutHelper(OutArrayProperty, OutArray);
OutHelper.EmptyValues();
const int32 Num = SourceHelper.Num();
if (Num == 0 || Count <= 0)
{
return;
}
const FProperty* InnerProp = TargetArrayProperty->Inner;
const int32 Start = FMath::Clamp(StartIndex, 0, Num);
const int32 NumToCopy = FMath::Min(Count, Num - Start);
for (int32 Offset = 0; Offset < NumToCopy; ++Offset)
{
const int32 OutIndex = OutHelper.AddValue();
InnerProp->CopySingleValueToScriptVM(OutHelper.GetRawPtr(OutIndex), SourceHelper.GetRawPtr(Start + Offset));
}
}
void UDirectiveUtilArrayFunctionLibrary::GenericArray_Rotate(
void* TargetArray,
const FArrayProperty* ArrayProperty,
const int32 Shift)
{
if (!TargetArray || !ArrayProperty)
{
return;
}
FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
const int32 Num = ArrayHelper.Num();
if (Num <= 1)
{
return;
}
int32 Normalized = Shift % Num;
if (Normalized < 0)
{
Normalized += Num;
}
if (Normalized == 0)
{
return;
}
auto ReverseRange = [&ArrayHelper](int32 Low, int32 High)
{
while (Low < High)
{
ArrayHelper.SwapValues(Low, High);
++Low;
--High;
}
};
ReverseRange(0, Num - 1);
ReverseRange(0, Normalized - 1);
ReverseRange(Normalized, Num - 1);
}
void UDirectiveUtilArrayFunctionLibrary::GenericArray_GetDistinct(
const void* TargetArray,
const FArrayProperty* TargetArrayProperty,
void* OutArray,
const FArrayProperty* OutArrayProperty)
{
if (!TargetArray || !OutArray || !TargetArrayProperty || !OutArrayProperty)
{
return;
}
FScriptArrayHelper SourceHelper(TargetArrayProperty, TargetArray);
FScriptArrayHelper OutHelper(OutArrayProperty, OutArray);
OutHelper.EmptyValues();
const FProperty* InnerProp = TargetArrayProperty->Inner;
const int32 Num = SourceHelper.Num();
for (int32 SourceIndex = 0; SourceIndex < Num; ++SourceIndex)
{
const uint8* SourceElement = SourceHelper.GetRawPtr(SourceIndex);
bool bIsDuplicate = false;
for (int32 ExistingIndex = 0; ExistingIndex < OutHelper.Num(); ++ExistingIndex)
{
if (InnerProp->Identical(SourceElement, OutHelper.GetRawPtr(ExistingIndex)))
{
bIsDuplicate = true;
break;
}
}
if (!bIsDuplicate)
{
const int32 OutIndex = OutHelper.AddValue();
InnerProp->CopySingleValueToScriptVM(OutHelper.GetRawPtr(OutIndex), SourceElement);
}
}
}
int32 UDirectiveUtilArrayFunctionLibrary::GenericArray_CountOccurrences(
const void* TargetArray,
const FArrayProperty* ArrayProperty,
const void* ItemToCount)
{
if (!TargetArray || !ArrayProperty || !ItemToCount)
{
return 0;
}
FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
const FProperty* InnerProp = ArrayProperty->Inner;
int32 Count = 0;
for (int32 Index = 0; Index < ArrayHelper.Num(); ++Index)
{
if (InnerProp->Identical(ArrayHelper.GetRawPtr(Index), ItemToCount))
{
++Count;
}
}
return Count;
}
bool UDirectiveUtilArrayFunctionLibrary::GenericArray_GetMostCommon(
const void* TargetArray,
const FArrayProperty* ArrayProperty,
void* OutItemPtr,
int32* OutCount)
{
if (OutCount)
{
*OutCount = 0;
}
if (!TargetArray || !ArrayProperty)
{
return false;
}
FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
const FProperty* InnerProp = ArrayProperty->Inner;
const int32 Num = ArrayHelper.Num();
if (Num == 0)
{
if (OutItemPtr)
{
InnerProp->ClearValue(OutItemPtr);
}
return false;
}
int32 BestIndex = 0;
int32 BestCount = 0;
for (int32 Index = 0; Index < Num; ++Index)
{
int32 CurrentCount = 0;
for (int32 CompareIndex = 0; CompareIndex < Num; ++CompareIndex)
{
if (InnerProp->Identical(ArrayHelper.GetRawPtr(Index), ArrayHelper.GetRawPtr(CompareIndex)))
{
++CurrentCount;
}
}
if (CurrentCount > BestCount)
{
BestCount = CurrentCount;
BestIndex = Index;
}
}
if (OutItemPtr)
{
InnerProp->CopyCompleteValueFromScriptVM(OutItemPtr, ArrayHelper.GetRawPtr(BestIndex));
}
if (OutCount)
{
*OutCount = BestCount;
}
return true;
}

View File

@@ -0,0 +1,87 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilFunctionLibrary.h"
#include "HAL/PlatformApplicationMisc.h"
#include "Misc/CommandLine.h"
#include "Misc/ConfigCacheIni.h"
void UDirectiveUtilFunctionLibrary::GetChildClasses(const UClass* BaseClass, const bool bRecursive, TArray<UClass*>& DerivedClasses)
{
GetDerivedClasses(BaseClass, DerivedClasses, bRecursive);
}
void UDirectiveUtilFunctionLibrary::CopyTextToClipboard(const FText& Text)
{
const FString ClipboardText = Text.ToString();
FPlatformApplicationMisc::ClipboardCopy(*ClipboardText);
}
void UDirectiveUtilFunctionLibrary::CopyStringToClipboard(const FString& String)
{
FPlatformApplicationMisc::ClipboardCopy(*String);
}
FText UDirectiveUtilFunctionLibrary::GetTextFromClipboard()
{
FString ClipboardText;
FPlatformApplicationMisc::ClipboardPaste(ClipboardText);
return FText::FromString(ClipboardText);
}
FString UDirectiveUtilFunctionLibrary::GetStringFromClipboard()
{
FString ClipboardText;
FPlatformApplicationMisc::ClipboardPaste(ClipboardText);
return ClipboardText;
}
void UDirectiveUtilFunctionLibrary::ClearClipboard()
{
FPlatformApplicationMisc::ClipboardCopy(TEXT(""));
}
FString UDirectiveUtilFunctionLibrary::GetProjectVersion()
{
FString ProjectVersion;
GConfig->GetString(
TEXT("/Script/EngineSettings.GeneralProjectSettings"),
TEXT("ProjectVersion"),
ProjectVersion,
GGameIni);
return ProjectVersion;
}
bool UDirectiveUtilFunctionLibrary::IsRunningInEditor()
{
return GIsEditor;
}
bool UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(const FString& Switch)
{
return HasCommandLineSwitch(FCommandLine::Get(), Switch);
}
bool UDirectiveUtilFunctionLibrary::GetCommandLineOption(const FString& Key, FString& OutValue)
{
return GetCommandLineOption(FCommandLine::Get(), Key, OutValue);
}
bool UDirectiveUtilFunctionLibrary::HasCommandLineSwitch(const TCHAR* CommandLine, const FString& Switch)
{
if (Switch.IsEmpty())
{
return false;
}
return FParse::Param(CommandLine, *Switch);
}
bool UDirectiveUtilFunctionLibrary::GetCommandLineOption(const TCHAR* CommandLine, const FString& Key, FString& OutValue)
{
OutValue.Reset();
if (Key.IsEmpty())
{
return false;
}
return FParse::Value(CommandLine, *(Key + TEXT("=")), OutValue);
}

View File

@@ -0,0 +1,164 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilGameplayTagFunctionLibrary.h"
#include "GameplayTagsManager.h"
FGameplayTag UDirectiveUtilGameplayTagFunctionLibrary::GetTagDirectParent(const FGameplayTag& Tag)
{
if (!Tag.IsValid())
{
return FGameplayTag();
}
return Tag.RequestDirectParent();
}
FGameplayTagContainer UDirectiveUtilGameplayTagFunctionLibrary::GetTagParents(const FGameplayTag& Tag)
{
if (!Tag.IsValid())
{
return FGameplayTagContainer();
}
FGameplayTagContainer Parents = Tag.GetGameplayTagParents();
Parents.RemoveTag(Tag);
return Parents;
}
int32 UDirectiveUtilGameplayTagFunctionLibrary::GetTagDepth(const FGameplayTag& Tag)
{
return GetTagSegments(Tag).Num();
}
FString UDirectiveUtilGameplayTagFunctionLibrary::GetTagLeafName(const FGameplayTag& Tag)
{
const TArray<FString> Segments = GetTagSegments(Tag);
return Segments.Num() > 0 ? Segments.Last() : FString();
}
TArray<FString> UDirectiveUtilGameplayTagFunctionLibrary::GetTagSegments(const FGameplayTag& Tag)
{
TArray<FString> Segments;
if (!Tag.IsValid())
{
return Segments;
}
Tag.GetTagName().ToString().ParseIntoArray(Segments, TEXT("."), true);
return Segments;
}
FGameplayTagContainer UDirectiveUtilGameplayTagFunctionLibrary::GetTagChildren(const FGameplayTag& Tag)
{
if (!Tag.IsValid())
{
return FGameplayTagContainer();
}
return UGameplayTagsManager::Get().RequestGameplayTagChildren(Tag);
}
FGameplayTagContainer UDirectiveUtilGameplayTagFunctionLibrary::GetTagDirectChildren(const FGameplayTag& Tag)
{
FGameplayTagContainer DirectChildren;
if (!Tag.IsValid())
{
return DirectChildren;
}
const int32 DirectChildDepth = GetTagDepth(Tag) + 1;
for (const FGameplayTag& Child : GetTagChildren(Tag))
{
if (GetTagDepth(Child) == DirectChildDepth)
{
DirectChildren.AddTag(Child);
}
}
return DirectChildren;
}
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())
{
return FGameplayTag();
}
// A common prefix of two registered tags is itself registered (parents auto-register).
return FGameplayTag::RequestGameplayTag(FName(*Prefix), false);
}
FGameplayTag UDirectiveUtilGameplayTagFunctionLibrary::GetTagAtDepth(const FGameplayTag& Tag, const int32 Depth)
{
if (!Tag.IsValid() || Depth < 1)
{
return FGameplayTag();
}
const TArray<FString> Segments = GetTagSegments(Tag);
if (Depth >= Segments.Num())
{
return Tag;
}
FString Prefix = Segments[0];
for (int32 Index = 1; Index < Depth; ++Index)
{
Prefix += TEXT(".");
Prefix += Segments[Index];
}
// An ancestor of a registered tag is always registered itself (parents auto-register).
return FGameplayTag::RequestGameplayTag(FName(*Prefix), false);
}
FGameplayTagContainer UDirectiveUtilGameplayTagFunctionLibrary::GetTagSiblings(const FGameplayTag& Tag)
{
const FGameplayTag DirectParent = GetTagDirectParent(Tag);
if (!DirectParent.IsValid())
{
return FGameplayTagContainer();
}
FGameplayTagContainer Siblings = GetTagDirectChildren(DirectParent);
Siblings.RemoveTag(Tag);
return Siblings;
}
bool UDirectiveUtilGameplayTagFunctionLibrary::IsLeafTag(const FGameplayTag& Tag)
{
return Tag.IsValid() && GetTagChildren(Tag).IsEmpty();
}
TArray<FGameplayTag> UDirectiveUtilGameplayTagFunctionLibrary::FindRegisteredTags(const FString& Substring)
{
TArray<FGameplayTag> MatchingTags;
if (Substring.IsEmpty())
{
return MatchingTags;
}
FGameplayTagContainer AllTags;
UGameplayTagsManager::Get().RequestAllGameplayTags(AllTags, /*OnlyIncludeDictionaryTags*/ false);
for (const FGameplayTag& RegisteredTag : AllTags)
{
if (RegisteredTag.GetTagName().ToString().Contains(Substring))
{
MatchingTags.Add(RegisteredTag);
}
}
return MatchingTags;
}

View File

@@ -0,0 +1,218 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilInputFunctionLibrary.h"
#include "EnhancedInputSubsystems.h"
#include "InputMappingContext.h"
#include "DirectiveUtilLogChannels.h"
#include "Logging/StructuredLog.h"
#include "GameFramework/Controller.h"
#include "GameFramework/PlayerController.h"
#include "Engine/LocalPlayer.h"
bool UDirectiveUtilInputFunctionLibrary::TryGetEnhancedInputSubsystemFromController(
AController* PlayerController,
UEnhancedInputLocalPlayerSubsystem*& EnhancedInput)
{
if (!PlayerController)
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("PlayerController is null. Cannot set input mapping contexts."));
return false;
}
const APlayerController* PC = Cast<APlayerController>(PlayerController);
if (!PC)
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller is not a PlayerController. Cannot get enhanced input subsystem."));
return false;
}
const ULocalPlayer* LocalPlayer = PC->GetLocalPlayer();
if (!LocalPlayer)
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("LocalPlayer not found. Cannot set input mapping contexts."));
return false;
}
EnhancedInput = LocalPlayer->GetSubsystem<UEnhancedInputLocalPlayerSubsystem>();
if(!EnhancedInput)
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("EnhancedInput subsystem not found. Cannot remove input mapping contexts."));
return false;
}
return true;
}
EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::AddInputMappingContexts(
AController* PlayerController,
const TArray<FDirectiveUtilEnhancedInputContextData>& Contexts,
const bool bClearPrevious)
{
if (Contexts.IsEmpty()) { return EDirectiveUtilSuccessStatus::Failure; }
UEnhancedInputLocalPlayerSubsystem* EnhancedInput;
const bool bEnhancedInputRetrievedFromController = TryGetEnhancedInputSubsystemFromController(PlayerController, EnhancedInput);
if(!bEnhancedInputRetrievedFromController)
{
return EDirectiveUtilSuccessStatus::Failure;
}
TArray<TPair<const UInputMappingContext*, int32>> LoadedContexts;
TArray<int32> FailedIndices;
for (int32 Index = 0; Index < Contexts.Num(); ++Index)
{
const auto& [InputContext, Priority] = Contexts[Index];
if (const UInputMappingContext* MappingContext = InputContext.LoadSynchronous())
{
LoadedContexts.Emplace(MappingContext, Priority);
}
else
{
FailedIndices.Add(Index);
}
}
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);
}
if (LoadedContexts.IsEmpty())
{
return EDirectiveUtilSuccessStatus::Failure;
}
if (bClearPrevious)
{
EnhancedInput->ClearAllMappings();
}
for (const auto& [MappingContext, Priority] : LoadedContexts)
{
EnhancedInput->AddMappingContext(MappingContext, Priority);
}
UE_LOGFMT(LogDirectiveUtil, Verbose, "{MappingCount} Input mapping contexts set successfully.", Contexts.Num() - FailedIndices.Num());
return EDirectiveUtilSuccessStatus::Success;
}
EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::RemoveInputMappingContexts(
AController* PlayerController,
const TArray<TSoftObjectPtr<UInputMappingContext>>& Contexts)
{
if (Contexts.IsEmpty()) { return EDirectiveUtilSuccessStatus::Failure;; }
UEnhancedInputLocalPlayerSubsystem* EnhancedInput;
const bool bEnhancedInputRetrievedFromController = TryGetEnhancedInputSubsystemFromController(PlayerController, EnhancedInput);
if(!bEnhancedInputRetrievedFromController)
{
return EDirectiveUtilSuccessStatus::Failure;
}
TArray<int32> FailedIndices;
for (int32 Index = 0; Index < Contexts.Num(); ++Index)
{
const TSoftObjectPtr<UInputMappingContext>& Context = Contexts[Index];
if (const UInputMappingContext* MappingContext = Context.LoadSynchronous())
{
EnhancedInput->RemoveMappingContext(MappingContext);
} else
{
FailedIndices.Add(Index);
}
}
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);
}
if (FailedIndices.Num() == Contexts.Num())
{
return EDirectiveUtilSuccessStatus::Failure;
}
UE_LOGFMT(LogDirectiveUtil, Verbose, "{MappingCount} Input mapping contexts removed successfully.", Contexts.Num() - FailedIndices.Num());
return EDirectiveUtilSuccessStatus::Success;
}
EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::SwapInputMappingContexts(
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 int32 TargetPriority = bUsePreviousPriority ? PreviousPriority : Priority;
EnhancedInput->RemoveMappingContext(LoadedPreviousMappingContext);
EnhancedInput->AddMappingContext(LoadedNewMappingContext, TargetPriority);
UE_LOGFMT(LogDirectiveUtil, Verbose, "Input mapping contexts {PreviousContext} and {NewContext} swapped successfully at priority {Priority}.", LoadedPreviousMappingContext->GetName(), LoadedNewMappingContext->GetName(), TargetPriority);
}
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);
}
return EDirectiveUtilSuccessStatus::Success;
}
UEnhancedInputLocalPlayerSubsystem* UDirectiveUtilInputFunctionLibrary::GetEnhancedInputSubsystem(AController* PlayerController)
{
UEnhancedInputLocalPlayerSubsystem* EnhancedInput = nullptr;
TryGetEnhancedInputSubsystemFromController(PlayerController, EnhancedInput);
return EnhancedInput;
}
bool UDirectiveUtilInputFunctionLibrary::IsInputMappingContextActive(AController* PlayerController, TSoftObjectPtr<UInputMappingContext> Context)
{
const UInputMappingContext* MappingContext = Context.LoadSynchronous();
if (!MappingContext)
{
return false;
}
UEnhancedInputLocalPlayerSubsystem* EnhancedInput;
if (!TryGetEnhancedInputSubsystemFromController(PlayerController, EnhancedInput))
{
return false;
}
int32 OutPriority;
return EnhancedInput->HasMappingContext(MappingContext, OutPriority);
}
EDirectiveUtilSuccessStatus UDirectiveUtilInputFunctionLibrary::ClearAllInputMappingContexts(AController* PlayerController)
{
UEnhancedInputLocalPlayerSubsystem* EnhancedInput;
if (!TryGetEnhancedInputSubsystemFromController(PlayerController, EnhancedInput))
{
return EDirectiveUtilSuccessStatus::Failure;
}
EnhancedInput->ClearAllMappings();
return EDirectiveUtilSuccessStatus::Success;
}

View File

@@ -0,0 +1,205 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilMapFunctionLibrary.h"
void UDirectiveUtilMapFunctionLibrary::Map_FindOrAdd(const TMap<int32, int32>& TargetMap, const int32& Key, int32& Value)
{
checkNoEntry();
}
void UDirectiveUtilMapFunctionLibrary::Map_ClearValues(const TMap<int32, int32>& TargetMap)
{
checkNoEntry();
}
void UDirectiveUtilMapFunctionLibrary::Map_GetKeysByValue(const TMap<int32, int32>& TargetMap, const int32& Value, TArray<int32>& Keys)
{
checkNoEntry();
}
bool UDirectiveUtilMapFunctionLibrary::Map_HasValue(const TMap<int32, int32>& TargetMap, const int32& Value)
{
checkNoEntry();
return false;
}
int32 UDirectiveUtilMapFunctionLibrary::Map_RemoveKeys(const TMap<int32, int32>& TargetMap, const TArray<int32>& Keys)
{
checkNoEntry();
return 0;
}
void UDirectiveUtilMapFunctionLibrary::Map_Append(const TMap<int32, int32>& TargetMap, const TMap<int32, int32>& SourceMap, bool bOverwriteExisting)
{
checkNoEntry();
}
void UDirectiveUtilMapFunctionLibrary::GenericMap_FindOrAdd(
const void* TargetMap,
const FMapProperty* MapProperty,
const void* KeyPtr,
void* ValuePtr)
{
if (!TargetMap || !MapProperty || !KeyPtr)
{
return;
}
FScriptMapHelper MapHelper(MapProperty, TargetMap);
void* ValueInMap = MapHelper.FindOrAdd(KeyPtr);
if (ValuePtr && ValueInMap)
{
MapProperty->ValueProp->CopyCompleteValueFromScriptVM(ValuePtr, ValueInMap);
}
}
void UDirectiveUtilMapFunctionLibrary::GenericMap_ClearValues(
const void* TargetMap,
const FMapProperty* MapProperty)
{
if (!TargetMap || !MapProperty)
{
return;
}
FScriptMapHelper MapHelper(MapProperty, TargetMap);
const FProperty* ValueProp = MapProperty->ValueProp;
const int32 MaxIndex = MapHelper.GetMaxIndex();
for (int32 InternalIndex = 0; InternalIndex < MaxIndex; ++InternalIndex)
{
if (MapHelper.IsValidIndex(InternalIndex))
{
ValueProp->ClearValue(MapHelper.GetValuePtr(InternalIndex));
}
}
}
void UDirectiveUtilMapFunctionLibrary::GenericMap_GetKeysByValue(
const void* TargetMap,
const FMapProperty* MapProperty,
const void* ValuePtr,
const void* TargetArray,
const FArrayProperty* ArrayProperty)
{
if (!TargetArray || !ArrayProperty)
{
return;
}
FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
ArrayHelper.EmptyValues();
if (!TargetMap || !MapProperty || !ValuePtr || !ArrayProperty->Inner->SameType(MapProperty->KeyProp))
{
return;
}
FScriptMapHelper MapHelper(MapProperty, TargetMap);
const FProperty* ValueProp = MapProperty->ValueProp;
const FProperty* InnerProp = ArrayProperty->Inner;
const int32 MaxIndex = MapHelper.GetMaxIndex();
for (int32 InternalIndex = 0; InternalIndex < MaxIndex; ++InternalIndex)
{
if (MapHelper.IsValidIndex(InternalIndex) && ValueProp->Identical(MapHelper.GetValuePtr(InternalIndex), ValuePtr))
{
const int32 LastIndex = ArrayHelper.AddValue();
InnerProp->CopySingleValueToScriptVM(ArrayHelper.GetRawPtr(LastIndex), MapHelper.GetKeyPtr(InternalIndex));
}
}
}
bool UDirectiveUtilMapFunctionLibrary::GenericMap_HasValue(
const void* TargetMap,
const FMapProperty* MapProperty,
const void* ValuePtr)
{
if (!TargetMap || !MapProperty || !ValuePtr)
{
return false;
}
FScriptMapHelper MapHelper(MapProperty, TargetMap);
const FProperty* ValueProp = MapProperty->ValueProp;
const int32 MaxIndex = MapHelper.GetMaxIndex();
for (int32 InternalIndex = 0; InternalIndex < MaxIndex; ++InternalIndex)
{
if (MapHelper.IsValidIndex(InternalIndex) && ValueProp->Identical(MapHelper.GetValuePtr(InternalIndex), ValuePtr))
{
return true;
}
}
return false;
}
int32 UDirectiveUtilMapFunctionLibrary::GenericMap_RemoveKeys(
const void* TargetMap,
const FMapProperty* MapProperty,
const void* TargetArray,
const FArrayProperty* ArrayProperty)
{
if (!TargetMap || !MapProperty || !TargetArray || !ArrayProperty
|| !ArrayProperty->Inner->SameType(MapProperty->KeyProp))
{
return 0;
}
FScriptMapHelper MapHelper(MapProperty, TargetMap);
FScriptArrayHelper ArrayHelper(ArrayProperty, TargetArray);
int32 NumRemoved = 0;
const int32 Num = ArrayHelper.Num();
for (int32 Index = 0; Index < Num; ++Index)
{
if (MapHelper.RemovePair(ArrayHelper.GetRawPtr(Index)))
{
++NumRemoved;
}
}
return NumRemoved;
}
void UDirectiveUtilMapFunctionLibrary::GenericMap_Append(
const void* TargetMap,
const FMapProperty* TargetMapProperty,
const void* SourceMap,
const FMapProperty* SourceMapProperty,
const bool bOverwriteExisting)
{
if (!TargetMap || !TargetMapProperty || !SourceMap || !SourceMapProperty
|| !TargetMapProperty->KeyProp->SameType(SourceMapProperty->KeyProp)
|| !TargetMapProperty->ValueProp->SameType(SourceMapProperty->ValueProp))
{
return;
}
// Appending a map onto itself is a no-op; bail before AddPair can reallocate under the source pointers.
if (TargetMap == SourceMap)
{
return;
}
FScriptMapHelper TargetHelper(TargetMapProperty, TargetMap);
FScriptMapHelper SourceHelper(SourceMapProperty, SourceMap);
const int32 MaxIndex = SourceHelper.GetMaxIndex();
for (int32 InternalIndex = 0; InternalIndex < MaxIndex; ++InternalIndex)
{
if (!SourceHelper.IsValidIndex(InternalIndex))
{
continue;
}
const uint8* KeyPtr = SourceHelper.GetKeyPtr(InternalIndex);
if (bOverwriteExisting || !TargetHelper.FindValueFromHash(KeyPtr))
{
TargetHelper.AddPair(KeyPtr, SourceHelper.GetValuePtr(InternalIndex));
}
}
}

View File

@@ -0,0 +1,466 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilMathFunctionLibrary.h"
namespace
{
double EaseBackIn(double t)
{
const double s = 1.70158;
return t * t * ((s + 1.0) * t - s);
}
double EaseBackOut(double t)
{
const double s = 1.70158;
t -= 1.0;
return t * t * ((s + 1.0) * t + s) + 1.0;
}
double EaseBackInOut(double t)
{
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;
}
}
float UDirectiveUtilMathFunctionLibrary::PerlinNoise2D(const FVector2D Position)
{
return FMath::PerlinNoise2D(Position);
}
float UDirectiveUtilMathFunctionLibrary::PerlinNoise3D(const FVector& Position)
{
return FMath::PerlinNoise3D(Position);
}
float UDirectiveUtilMathFunctionLibrary::AngleBetweenVectors(const FVector& A, const FVector& B)
{
return FMath::RadiansToDegrees(FMath::Acos(FMath::Clamp(FVector::DotProduct(A.GetSafeNormal(), B.GetSafeNormal()), -1.0, 1.0)));
}
float UDirectiveUtilMathFunctionLibrary::EaseAlpha(const float Alpha, const EDirectiveUtilEaseType EaseType)
{
const double t = static_cast<double>(FMath::Clamp(Alpha, 0.0f, 1.0f));
double Result = t;
switch (EaseType)
{
case EDirectiveUtilEaseType::BackIn: Result = EaseBackIn(t); break;
case EDirectiveUtilEaseType::BackOut: Result = EaseBackOut(t); break;
case EDirectiveUtilEaseType::BackInOut: Result = EaseBackInOut(t); break;
case EDirectiveUtilEaseType::ElasticIn: Result = EaseElasticIn(t); break;
case EDirectiveUtilEaseType::ElasticOut: Result = EaseElasticOut(t); break;
case EDirectiveUtilEaseType::ElasticInOut: Result = EaseElasticInOut(t); break;
case EDirectiveUtilEaseType::BounceIn: Result = EaseBounceIn(t); break;
case EDirectiveUtilEaseType::BounceOut: Result = EaseBounceOut(t); break;
case EDirectiveUtilEaseType::BounceInOut: Result = EaseBounceInOut(t); break;
}
return static_cast<float>(Result);
}
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));
}
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 int64 TotalSeconds = static_cast<int64>(FMath::Abs(static_cast<double>(Seconds)));
const bool bNegative = Seconds < 0.0f && TotalSeconds > 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 to whole seconds so clock-adjacent inputs (e.g. Now() + 2 hours) land in the intended 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> Sorted = Values;
Sorted.Sort();
const int32 Middle = Sorted.Num() / 2;
if (Sorted.Num() % 2 == 0)
{
return static_cast<float>((static_cast<double>(Sorted[Middle - 1]) + static_cast<double>(Sorted[Middle])) * 0.5);
}
return static_cast<float>(Sorted[Middle]);
}
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> Sorted = Values;
Sorted.Sort();
const int32 Middle = Sorted.Num() / 2;
if (Sorted.Num() % 2 == 0)
{
return static_cast<float>((static_cast<double>(Sorted[Middle - 1]) + static_cast<double>(Sorted[Middle])) * 0.5);
}
return Sorted[Middle];
}
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()));
}
int32 UDirectiveUtilMathFunctionLibrary::GetRandomIndexFromWeights(const TArray<float>& Weights)
{
float Total = 0.0f;
for (const float Weight : Weights)
{
Total += FMath::Max(0.0f, Weight);
}
if (Total <= 0.0f)
{
return INDEX_NONE;
}
const float Roll = FMath::FRand() * Total;
float Accumulated = 0.0f;
int32 LastPositiveIndex = INDEX_NONE;
for (int32 Index = 0; Index < Weights.Num(); ++Index)
{
const float Weight = FMath::Max(0.0f, 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)
{
float Total = 0.0f;
for (const float Weight : Weights)
{
Total += FMath::Max(0.0f, Weight);
}
if (Total <= 0.0f)
{
return INDEX_NONE;
}
const float Roll = Stream.FRand() * Total;
float Accumulated = 0.0f;
int32 LastPositiveIndex = INDEX_NONE;
for (int32 Index = 0; Index < Weights.Num(); ++Index)
{
const float Weight = FMath::Max(0.0f, Weights[Index]);
if (Weight <= 0.0f)
{
continue;
}
LastPositiveIndex = Index;
Accumulated += Weight;
if (Roll < Accumulated)
{
return Index;
}
}
return LastPositiveIndex;
}

View File

@@ -0,0 +1,131 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilRegexFunctionLibrary.h"
#include "Internationalization/Regex.h"
namespace
{
FRegexPattern MakePattern(const FString& Pattern, const bool bCaseSensitive)
{
return FRegexPattern(Pattern, bCaseSensitive ? ERegexPatternFlags::None : ERegexPatternFlags::CaseInsensitive);
}
}
bool UDirectiveUtilRegexFunctionLibrary::RegexMatches(const FString& Input, const FString& Pattern, const bool bCaseSensitive)
{
if (Pattern.IsEmpty())
{
return false;
}
const FRegexPattern RegexPattern = MakePattern(Pattern, bCaseSensitive);
FRegexMatcher Matcher(RegexPattern, Input);
return Matcher.FindNext();
}
bool UDirectiveUtilRegexFunctionLibrary::RegexFindFirst(const FString& Input, const FString& Pattern, FString& OutMatch, int32& OutMatchStart, int32& OutMatchEnd, const bool bCaseSensitive)
{
OutMatch = FString();
OutMatchStart = INDEX_NONE;
OutMatchEnd = INDEX_NONE;
if (Pattern.IsEmpty())
{
return false;
}
const FRegexPattern RegexPattern = MakePattern(Pattern, bCaseSensitive);
FRegexMatcher Matcher(RegexPattern, Input);
if (Matcher.FindNext())
{
OutMatchStart = Matcher.GetMatchBeginning();
OutMatchEnd = Matcher.GetMatchEnding();
OutMatch = Matcher.GetCaptureGroup(0);
return true;
}
return false;
}
TArray<FString> UDirectiveUtilRegexFunctionLibrary::RegexFindAll(const FString& Input, const FString& Pattern, const bool bCaseSensitive)
{
TArray<FString> Matches;
if (Pattern.IsEmpty())
{
return Matches;
}
const FRegexPattern RegexPattern = MakePattern(Pattern, bCaseSensitive);
FRegexMatcher Matcher(RegexPattern, Input);
while (Matcher.FindNext())
{
const int32 Begin = Matcher.GetMatchBeginning();
const int32 End = Matcher.GetMatchEnding();
if (Begin == End)
{
continue;
}
Matches.Add(Matcher.GetCaptureGroup(0));
}
return Matches;
}
FString UDirectiveUtilRegexFunctionLibrary::RegexReplaceAll(const FString& Input, const FString& Pattern, const FString& Replacement, const bool bCaseSensitive)
{
if (Pattern.IsEmpty())
{
return Input;
}
const FRegexPattern RegexPattern = MakePattern(Pattern, bCaseSensitive);
FRegexMatcher Matcher(RegexPattern, Input);
FString Result;
int32 LastEnd = 0;
while (Matcher.FindNext())
{
const int32 Begin = Matcher.GetMatchBeginning();
const int32 End = Matcher.GetMatchEnding();
if (Begin == End)
{
continue;
}
if (Begin < LastEnd)
{
continue;
}
Result.Append(Input.Mid(LastEnd, Begin - LastEnd));
Result.Append(Replacement);
LastEnd = End;
}
Result.Append(Input.Mid(LastEnd));
return Result;
}
bool UDirectiveUtilRegexFunctionLibrary::RegexGetCaptureGroup(const FString& Input, const FString& Pattern, const int32 GroupIndex, FString& OutGroup, const bool bCaseSensitive)
{
OutGroup = FString();
if (Pattern.IsEmpty() || GroupIndex < 0)
{
return false;
}
const FRegexPattern RegexPattern = MakePattern(Pattern, bCaseSensitive);
FRegexMatcher Matcher(RegexPattern, Input);
if (!Matcher.FindNext())
{
return false;
}
if (Matcher.GetCaptureGroupBeginning(GroupIndex) == INDEX_NONE)
{
return false;
}
OutGroup = Matcher.GetCaptureGroup(GroupIndex);
return true;
}

View File

@@ -0,0 +1,121 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilSaveGameFunctionLibrary.h"
#include "Libraries/DirectiveUtilStringFunctionLibrary.h"
#include "Kismet/GameplayStatics.h"
#include "GameFramework/SaveGame.h"
#include "HAL/FileManager.h"
#include "Misc/Paths.h"
namespace
{
FString GetSaveGamesDirectory()
{
return FPaths::ProjectSavedDir() / TEXT("SaveGames");
}
FString GetSaveSlotFilePath(const FString& SlotName)
{
return GetSaveGamesDirectory() / (SlotName + TEXT(".sav"));
}
bool IsValidSaveSlotName(const FString& SlotName)
{
return UDirectiveUtilStringFunctionLibrary::IsValidFileName(SlotName);
}
}
TArray<FString> UDirectiveUtilSaveGameFunctionLibrary::GetAllSaveSlotNames()
{
TArray<FString> SlotNames;
TArray<FString> Files;
IFileManager::Get().FindFiles(Files, *(GetSaveGamesDirectory() / TEXT("*.sav")), true, false);
SlotNames.Reserve(Files.Num());
for (const FString& File : Files)
{
SlotNames.Add(FPaths::GetBaseFilename(File));
}
return SlotNames;
}
bool UDirectiveUtilSaveGameFunctionLibrary::GetSaveSlotTimestamp(const FString& SlotName, FDateTime& OutTimestamp)
{
OutTimestamp = FDateTime();
if (!IsValidSaveSlotName(SlotName))
{
return false;
}
const FDateTime Timestamp = IFileManager::Get().GetTimeStamp(*GetSaveSlotFilePath(SlotName));
if (Timestamp == FDateTime::MinValue())
{
return false;
}
OutTimestamp = Timestamp + (FDateTime::Now() - FDateTime::UtcNow());
return true;
}
bool UDirectiveUtilSaveGameFunctionLibrary::SaveGameToBytes(USaveGame* SaveGameObject, TArray<uint8>& OutBytes)
{
OutBytes.Reset();
if (!SaveGameObject)
{
return false;
}
return UGameplayStatics::SaveGameToMemory(SaveGameObject, OutBytes);
}
USaveGame* UDirectiveUtilSaveGameFunctionLibrary::LoadGameFromBytes(const TArray<uint8>& SaveData)
{
if (SaveData.Num() == 0)
{
return nullptr;
}
return UGameplayStatics::LoadGameFromMemory(SaveData);
}
bool UDirectiveUtilSaveGameFunctionLibrary::DoesSaveSlotExist(const FString& SlotName, const int32 UserIndex)
{
if (!IsValidSaveSlotName(SlotName))
{
return false;
}
return UGameplayStatics::DoesSaveGameExist(SlotName, UserIndex);
}
bool UDirectiveUtilSaveGameFunctionLibrary::DeleteSaveSlot(const FString& SlotName, const int32 UserIndex)
{
if (!IsValidSaveSlotName(SlotName))
{
return false;
}
return UGameplayStatics::DeleteGameInSlot(SlotName, UserIndex);
}
bool UDirectiveUtilSaveGameFunctionLibrary::RenameSaveSlot(const FString& OldSlotName, const FString& NewSlotName, const int32 UserIndex)
{
if (!IsValidSaveSlotName(OldSlotName) || !IsValidSaveSlotName(NewSlotName) || OldSlotName == NewSlotName)
{
return false;
}
if (!UGameplayStatics::DoesSaveGameExist(OldSlotName, UserIndex) || UGameplayStatics::DoesSaveGameExist(NewSlotName, UserIndex))
{
return false;
}
TArray<uint8> SaveData;
if (!UGameplayStatics::LoadDataFromSlot(SaveData, OldSlotName, UserIndex))
{
return false;
}
if (!UGameplayStatics::SaveDataToSlot(SaveData, NewSlotName, UserIndex))
{
return false;
}
// If this delete fails the new copy is kept alongside the original, so the save is never lost.
return UGameplayStatics::DeleteGameInSlot(OldSlotName, UserIndex);
}

View File

@@ -0,0 +1,422 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilStringFunctionLibrary.h"
#include "Misc/Base64.h"
#include "Misc/Crc.h"
#include "Misc/Paths.h"
#include "Misc/SecureHash.h"
namespace
{
TArray<uint8> StringToUtf8Bytes(const FString& String)
{
const FTCHARToUTF8 Converter(*String, String.Len());
return TArray<uint8>(reinterpret_cast<const uint8*>(Converter.Get()), Converter.Length());
}
}
bool UDirectiveUtilStringFunctionLibrary::ContainsLetters(const FString& String)
{
for (const TCHAR& Char : String)
{
if (FChar::IsAlpha(Char))
{
return true;
}
}
return false;
}
bool UDirectiveUtilStringFunctionLibrary::ContainsNumbers(const FString& String)
{
for (const TCHAR& Char : String)
{
if (FChar::IsDigit(Char))
{
return true;
}
}
return false;
}
bool UDirectiveUtilStringFunctionLibrary::ContainsSpaces(const FString& String)
{
for (const TCHAR& Char : String)
{
if (FChar::IsWhitespace(Char))
{
return true;
}
}
return false;
}
bool UDirectiveUtilStringFunctionLibrary::ContainsSpecialCharacters(const FString& String)
{
for (const TCHAR& Char : String)
{
if (FChar::IsPunct(Char))
{
return true;
}
}
return false;
}
FString UDirectiveUtilStringFunctionLibrary::FilterCharacters(
const FString& String,
const bool bLetters,
const bool bNumbers,
const bool bSpecialCharacters,
const bool bSpaces)
{
FString NewString;
NewString.Reserve(String.Len());
for (const TCHAR& Char : String)
{
if (bLetters && FChar::IsAlpha(Char)) continue;
if (bNumbers && FChar::IsDigit(Char)) continue;
if (bSpecialCharacters && FChar::IsPunct(Char)) continue;
if (bSpaces && FChar::IsWhitespace(Char)) continue;
NewString.AppendChar(Char);
}
return NewString;
}
FString UDirectiveUtilStringFunctionLibrary::TruncateString(const FString& String, const int32 MaxLength, const FString& Suffix)
{
if (String.Len() <= MaxLength)
{
return String;
}
return String.Left(FMath::Max(0, MaxLength - Suffix.Len())) + Suffix;
}
FString UDirectiveUtilStringFunctionLibrary::ToTitleCase(const FString& String)
{
FString Result;
Result.Reserve(String.Len());
bool bCapitalizeNext = true;
for (const TCHAR& Char : String)
{
if (FChar::IsWhitespace(Char))
{
bCapitalizeNext = true;
Result.AppendChar(Char);
}
else if (bCapitalizeNext && FChar::IsAlpha(Char))
{
Result.AppendChar(FChar::ToUpper(Char));
bCapitalizeNext = false;
}
else
{
Result.AppendChar(FChar::ToLower(Char));
bCapitalizeNext = false;
}
}
return Result;
}
TArray<FString> UDirectiveUtilStringFunctionLibrary::SplitIntoWords(const FString& String)
{
TArray<FString> Words;
FString CurrentWord;
for (int32 Index = 0; Index < String.Len(); ++Index)
{
const TCHAR Char = String[Index];
if (!FChar::IsAlnum(Char))
{
if (!CurrentWord.IsEmpty())
{
Words.Add(MoveTemp(CurrentWord));
CurrentWord.Reset();
}
continue;
}
if (!CurrentWord.IsEmpty())
{
const TCHAR Previous = CurrentWord[CurrentWord.Len() - 1];
const bool bNextIsLower = Index + 1 < String.Len() && FChar::IsLower(String[Index + 1]);
const bool bBoundary =
(FChar::IsLower(Previous) && FChar::IsUpper(Char)) ||
(FChar::IsUpper(Previous) && FChar::IsUpper(Char) && bNextIsLower) ||
(FChar::IsAlpha(Previous) && FChar::IsDigit(Char)) ||
(FChar::IsDigit(Previous) && FChar::IsAlpha(Char));
if (bBoundary)
{
Words.Add(MoveTemp(CurrentWord));
CurrentWord.Reset();
}
}
CurrentWord.AppendChar(Char);
}
if (!CurrentWord.IsEmpty())
{
Words.Add(MoveTemp(CurrentWord));
}
return Words;
}
FString UDirectiveUtilStringFunctionLibrary::ToCamelCase(const FString& String)
{
const TArray<FString> Words = SplitIntoWords(String);
FString Result;
for (int32 Index = 0; Index < Words.Num(); ++Index)
{
FString Word = Words[Index].ToLower();
if (Index > 0)
{
Word[0] = FChar::ToUpper(Word[0]);
}
Result += Word;
}
return Result;
}
FString UDirectiveUtilStringFunctionLibrary::ToPascalCase(const FString& String)
{
FString Result;
for (const FString& Word : SplitIntoWords(String))
{
FString Cased = Word.ToLower();
Cased[0] = FChar::ToUpper(Cased[0]);
Result += Cased;
}
return Result;
}
FString UDirectiveUtilStringFunctionLibrary::ToSnakeCase(const FString& String)
{
return FString::Join(SplitIntoWords(String), TEXT("_")).ToLower();
}
FString UDirectiveUtilStringFunctionLibrary::ToKebabCase(const FString& String)
{
return FString::Join(SplitIntoWords(String), TEXT("-")).ToLower();
}
TArray<FString> UDirectiveUtilStringFunctionLibrary::SortStringArray(TArray<FString> StringArray)
{
Algo::Sort(StringArray);
return StringArray;
}
TArray<FString> UDirectiveUtilStringFunctionLibrary::GetSortedStringArray(const TArray<FString> StringArray)
{
TArray<FString> SortedArray = StringArray;
Algo::Sort(SortedArray);
return SortedArray;
}
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; }
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];
}
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));
}
bool UDirectiveUtilStringFunctionLibrary::ContainsAny(const FString& Source, const TArray<FString>& SearchTerms, const bool bCaseSensitive)
{
const ESearchCase::Type SearchCase = bCaseSensitive ? ESearchCase::CaseSensitive : ESearchCase::IgnoreCase;
for (const FString& Term : SearchTerms)
{
if (!Term.IsEmpty() && Source.Contains(Term, SearchCase))
{
return true;
}
}
return false;
}
bool UDirectiveUtilStringFunctionLibrary::FindFirstOfAny(const FString& Source, const TArray<FString>& SearchTerms, const bool bCaseSensitive, int32& OutFoundIndex, int32& OutTermIndex)
{
OutFoundIndex = INDEX_NONE;
OutTermIndex = INDEX_NONE;
const ESearchCase::Type SearchCase = bCaseSensitive ? ESearchCase::CaseSensitive : ESearchCase::IgnoreCase;
for (int32 TermIndex = 0; TermIndex < SearchTerms.Num(); ++TermIndex)
{
const FString& Term = SearchTerms[TermIndex];
if (Term.IsEmpty())
{
continue;
}
const int32 FoundIndex = Source.Find(Term, SearchCase, ESearchDir::FromStart);
if (FoundIndex != INDEX_NONE && (OutFoundIndex == INDEX_NONE || FoundIndex < OutFoundIndex))
{
OutFoundIndex = FoundIndex;
OutTermIndex = TermIndex;
}
}
return OutFoundIndex != INDEX_NONE;
}
FString UDirectiveUtilStringFunctionLibrary::Base64Encode(const FString& Source)
{
return FBase64::Encode(Source);
}
bool UDirectiveUtilStringFunctionLibrary::Base64Decode(const FString& Source, FString& OutDecoded)
{
OutDecoded.Reset();
return FBase64::Decode(Source, OutDecoded);
}
FString UDirectiveUtilStringFunctionLibrary::HexEncode(const FString& String)
{
return HexEncodeBytes(StringToUtf8Bytes(String));
}
bool UDirectiveUtilStringFunctionLibrary::HexDecode(const FString& Hex, FString& OutString)
{
OutString.Reset();
TArray<uint8> Bytes;
if (!HexDecodeBytes(Hex, Bytes))
{
return false;
}
const FUTF8ToTCHAR Converter(reinterpret_cast<const ANSICHAR*>(Bytes.GetData()), Bytes.Num());
OutString = FString(Converter.Length(), Converter.Get());
return true;
}
FString UDirectiveUtilStringFunctionLibrary::HexEncodeBytes(const TArray<uint8>& Bytes)
{
return BytesToHexLower(Bytes.GetData(), Bytes.Num());
}
bool UDirectiveUtilStringFunctionLibrary::HexDecodeBytes(const FString& Hex, TArray<uint8>& OutBytes)
{
OutBytes.Reset();
if (Hex.Len() % 2 != 0)
{
return false;
}
OutBytes.Reserve(Hex.Len() / 2);
for (int32 Index = 0; Index < Hex.Len(); Index += 2)
{
const TCHAR High = Hex[Index];
const TCHAR Low = Hex[Index + 1];
if (!CheckTCharIsHex(High) || !CheckTCharIsHex(Low))
{
OutBytes.Reset();
return false;
}
OutBytes.Add(static_cast<uint8>((TCharToNibble(High) << 4) | TCharToNibble(Low)));
}
return true;
}
FString UDirectiveUtilStringFunctionLibrary::Md5HashString(const FString& String)
{
return Md5HashBytes(StringToUtf8Bytes(String));
}
FString UDirectiveUtilStringFunctionLibrary::Md5HashBytes(const TArray<uint8>& Bytes)
{
return FMD5::HashBytes(Bytes.GetData(), Bytes.Num());
}
FString UDirectiveUtilStringFunctionLibrary::Sha1HashString(const FString& String)
{
return Sha1HashBytes(StringToUtf8Bytes(String));
}
FString UDirectiveUtilStringFunctionLibrary::Sha1HashBytes(const TArray<uint8>& Bytes)
{
uint8 Digest[20];
FSHA1::HashBuffer(Bytes.GetData(), Bytes.Num(), Digest);
return BytesToHexLower(Digest, UE_ARRAY_COUNT(Digest));
}
int32 UDirectiveUtilStringFunctionLibrary::Crc32String(const FString& String)
{
return Crc32Bytes(StringToUtf8Bytes(String));
}
int32 UDirectiveUtilStringFunctionLibrary::Crc32Bytes(const TArray<uint8>& Bytes)
{
return static_cast<int32>(FCrc::MemCrc32(Bytes.GetData(), Bytes.Num()));
}
bool UDirectiveUtilStringFunctionLibrary::IsValidFileName(const FString& String)
{
return !String.IsEmpty() && 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]);
}
int32 UDirectiveUtilStringFunctionLibrary::FindBestStringMatch(const FString& Input, const TArray<FString>& Candidates, float& OutSimilarity, const bool bCaseSensitive)
{
OutSimilarity = 0.0f;
int32 BestIndex = INDEX_NONE;
for (int32 Index = 0; Index < Candidates.Num(); ++Index)
{
const float Similarity = GetStringSimilarity(Input, Candidates[Index], bCaseSensitive);
if (BestIndex == INDEX_NONE || Similarity > OutSimilarity)
{
BestIndex = Index;
OutSimilarity = Similarity;
}
}
return BestIndex;
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Libraries/DirectiveUtilTextFunctionLibrary.h"
bool UDirectiveUtilTextFunctionLibrary::IsNotEmpty(const FText& Text)
{
return !Text.IsEmpty();
}

View File

@@ -0,0 +1,245 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Tasks/DirectiveUtilTask_AsyncLoadAsset.h"
#include "DirectiveUtilLogChannels.h"
#include "Engine/AssetManager.h"
#include "Engine/StreamableManager.h"
UDirectiveUtilTask_AsyncLoadAsset* UDirectiveUtilTask_AsyncLoadAsset::AsyncLoadAsset(UObject* WorldContextObject, const TSoftObjectPtr<UObject> Asset)
{
UDirectiveUtilTask_AsyncLoadAsset* Action = NewObject<UDirectiveUtilTask_AsyncLoadAsset>();
Action->SoftAsset = Asset;
if (WorldContextObject)
{
Action->RegisterWithGameInstance(WorldContextObject);
}
return Action;
}
void UDirectiveUtilTask_AsyncLoadAsset::Activate()
{
if (SoftAsset.IsNull())
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Async Load Asset failed to activate. The soft object reference is null."));
Failed.Broadcast(nullptr);
SetReadyToDestroy();
return;
}
if (!UAssetManager::GetIfInitialized())
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Async Load Asset failed to activate. The Asset Manager is not initialized."));
Failed.Broadcast(nullptr);
SetReadyToDestroy();
return;
}
StreamableHandle = UAssetManager::GetStreamableManager().RequestAsyncLoad(
SoftAsset.ToSoftObjectPath(),
FStreamableDelegate::CreateUObject(this, &UDirectiveUtilTask_AsyncLoadAsset::OnLoaded),
FStreamableManager::DefaultAsyncLoadPriority);
if (!StreamableHandle.IsValid())
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Async Load Asset failed to start the load request."));
Failed.Broadcast(nullptr);
SetReadyToDestroy();
}
}
void UDirectiveUtilTask_AsyncLoadAsset::OnLoaded()
{
UObject* LoadedAsset = StreamableHandle.IsValid() ? StreamableHandle->GetLoadedAsset() : nullptr;
if (LoadedAsset)
{
Completed.Broadcast(LoadedAsset);
}
else
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Async Load Asset completed but the asset could not be resolved."));
Failed.Broadcast(nullptr);
}
SetReadyToDestroy();
}
void UDirectiveUtilTask_AsyncLoadAsset::Cancel()
{
if (StreamableHandle.IsValid() && StreamableHandle->IsActive())
{
StreamableHandle->CancelHandle();
}
SetReadyToDestroy();
}
UDirectiveUtilTask_AsyncLoadClass* UDirectiveUtilTask_AsyncLoadClass::AsyncLoadClass(UObject* WorldContextObject, const TSoftClassPtr<UObject> AssetClass)
{
UDirectiveUtilTask_AsyncLoadClass* Action = NewObject<UDirectiveUtilTask_AsyncLoadClass>();
Action->SoftClass = AssetClass;
if (WorldContextObject)
{
Action->RegisterWithGameInstance(WorldContextObject);
}
return Action;
}
void UDirectiveUtilTask_AsyncLoadClass::Activate()
{
if (SoftClass.IsNull())
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Async Load Class failed to activate. The soft class reference is null."));
Failed.Broadcast(nullptr);
SetReadyToDestroy();
return;
}
if (!UAssetManager::GetIfInitialized())
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Async Load Class failed to activate. The Asset Manager is not initialized."));
Failed.Broadcast(nullptr);
SetReadyToDestroy();
return;
}
StreamableHandle = UAssetManager::GetStreamableManager().RequestAsyncLoad(
SoftClass.ToSoftObjectPath(),
FStreamableDelegate::CreateUObject(this, &UDirectiveUtilTask_AsyncLoadClass::OnLoaded),
FStreamableManager::DefaultAsyncLoadPriority);
if (!StreamableHandle.IsValid())
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Async Load Class failed to start the load request."));
Failed.Broadcast(nullptr);
SetReadyToDestroy();
}
}
void UDirectiveUtilTask_AsyncLoadClass::OnLoaded()
{
UClass* LoadedClass = StreamableHandle.IsValid() ? Cast<UClass>(StreamableHandle->GetLoadedAsset()) : nullptr;
if (LoadedClass)
{
Completed.Broadcast(LoadedClass);
}
else
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Async Load Class completed but the class could not be resolved."));
Failed.Broadcast(nullptr);
}
SetReadyToDestroy();
}
void UDirectiveUtilTask_AsyncLoadClass::Cancel()
{
if (StreamableHandle.IsValid() && StreamableHandle->IsActive())
{
StreamableHandle->CancelHandle();
}
SetReadyToDestroy();
}
UDirectiveUtilTask_AsyncLoadAssets* UDirectiveUtilTask_AsyncLoadAssets::AsyncLoadAssets(UObject* WorldContextObject, const TArray<TSoftObjectPtr<UObject>>& Assets)
{
UDirectiveUtilTask_AsyncLoadAssets* Action = NewObject<UDirectiveUtilTask_AsyncLoadAssets>();
Action->SoftAssets = Assets;
if (WorldContextObject)
{
Action->RegisterWithGameInstance(WorldContextObject);
}
return Action;
}
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)
{
if (!SoftAsset.IsNull())
{
PathsToLoad.AddUnique(SoftAsset.ToSoftObjectPath());
}
}
if (PathsToLoad.Num() == 0)
{
// Nothing to request; an empty request list is an error path in the streamable manager.
OnLoaded();
return;
}
if (!UAssetManager::GetIfInitialized())
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Async Load Assets failed to activate. The Asset Manager is not initialized."));
OnLoaded();
return;
}
StreamableHandle = UAssetManager::GetStreamableManager().RequestAsyncLoad(
MoveTemp(PathsToLoad),
FStreamableDelegate::CreateUObject(this, &UDirectiveUtilTask_AsyncLoadAssets::OnLoaded),
FStreamableManager::DefaultAsyncLoadPriority);
if (!StreamableHandle.IsValid())
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Async Load Assets failed to start the load request."));
OnLoaded();
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();
}
}
void UDirectiveUtilTask_AsyncLoadAssets::OnLoaded()
{
if (bHasCompleted)
{
return;
}
bHasCompleted = true;
TArray<UObject*> LoadedAssets;
LoadedAssets.Reserve(SoftAssets.Num());
for (const TSoftObjectPtr<UObject>& SoftAsset : SoftAssets)
{
LoadedAssets.Add(SoftAsset.Get());
}
Completed.Broadcast(LoadedAssets);
SetReadyToDestroy();
}
void UDirectiveUtilTask_AsyncLoadAssets::OnUpdate(TSharedRef<FStreamableHandle> Handle)
{
if (bHasCompleted)
{
return;
}
int32 LoadedCount = 0;
int32 RequestedCount = 0;
Handle->GetLoadedCount(LoadedCount, RequestedCount);
Progress.Broadcast(LoadedCount, RequestedCount);
}
void UDirectiveUtilTask_AsyncLoadAssets::Cancel()
{
bHasCompleted = true;
if (StreamableHandle.IsValid() && StreamableHandle->IsActive())
{
StreamableHandle->CancelHandle();
}
SetReadyToDestroy();
}

View File

@@ -0,0 +1,139 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Tasks/DirectiveUtilTask_AsyncTrace.h"
#include "DirectiveUtilLogChannels.h"
#include "Engine/Engine.h"
#include "Engine/World.h"
#include "WorldCollision.h"
#include "CollisionShape.h"
#include "CollisionQueryParams.h"
UDirectiveUtilTask_AsyncTrace* UDirectiveUtilTask_AsyncTrace::AsyncLineTraceByChannel(UObject* WorldContextObject, const FVector Start, const FVector End, const ETraceTypeQuery TraceChannel, const bool bMultiTrace)
{
UDirectiveUtilTask_AsyncTrace* Action = NewObject<UDirectiveUtilTask_AsyncTrace>();
Action->WorldContextObject = WorldContextObject;
Action->Start = Start;
Action->End = End;
Action->TraceChannel = TraceChannel;
Action->bMultiTrace = bMultiTrace;
Action->Shape = EDirectiveUtilTraceShape::Line;
if (WorldContextObject)
{
Action->RegisterWithGameInstance(WorldContextObject);
}
return Action;
}
UDirectiveUtilTask_AsyncTrace* UDirectiveUtilTask_AsyncTrace::AsyncSphereTraceByChannel(UObject* WorldContextObject, const FVector Start, const FVector End, const float Radius, const ETraceTypeQuery TraceChannel, const bool bMultiTrace)
{
UDirectiveUtilTask_AsyncTrace* Action = NewObject<UDirectiveUtilTask_AsyncTrace>();
Action->WorldContextObject = WorldContextObject;
Action->Start = Start;
Action->End = End;
Action->Radius = Radius;
Action->TraceChannel = TraceChannel;
Action->bMultiTrace = bMultiTrace;
Action->Shape = EDirectiveUtilTraceShape::Sphere;
if (WorldContextObject)
{
Action->RegisterWithGameInstance(WorldContextObject);
}
return Action;
}
UDirectiveUtilTask_AsyncTrace* UDirectiveUtilTask_AsyncTrace::AsyncBoxTraceByChannel(UObject* WorldContextObject, const FVector Start, const FVector End, const FVector HalfSize, const FRotator Orientation, const ETraceTypeQuery TraceChannel, const bool bMultiTrace)
{
UDirectiveUtilTask_AsyncTrace* Action = NewObject<UDirectiveUtilTask_AsyncTrace>();
Action->WorldContextObject = WorldContextObject;
Action->Start = Start;
Action->End = End;
Action->HalfSize = HalfSize;
Action->Orientation = Orientation.Quaternion();
Action->TraceChannel = TraceChannel;
Action->bMultiTrace = bMultiTrace;
Action->Shape = EDirectiveUtilTraceShape::Box;
if (WorldContextObject)
{
Action->RegisterWithGameInstance(WorldContextObject);
}
return Action;
}
UDirectiveUtilTask_AsyncTrace* UDirectiveUtilTask_AsyncTrace::AsyncCapsuleTraceByChannel(UObject* WorldContextObject, const FVector Start, const FVector End, const float Radius, const float HalfHeight, const ETraceTypeQuery TraceChannel, const bool bMultiTrace)
{
UDirectiveUtilTask_AsyncTrace* Action = NewObject<UDirectiveUtilTask_AsyncTrace>();
Action->WorldContextObject = WorldContextObject;
Action->Start = Start;
Action->End = End;
Action->Radius = Radius;
Action->HalfHeight = HalfHeight;
Action->TraceChannel = TraceChannel;
Action->bMultiTrace = bMultiTrace;
Action->Shape = EDirectiveUtilTraceShape::Capsule;
if (WorldContextObject)
{
Action->RegisterWithGameInstance(WorldContextObject);
}
return Action;
}
void UDirectiveUtilTask_AsyncTrace::Activate()
{
UWorld* World = WorldContextObject && GEngine
? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull)
: nullptr;
if (!World)
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Async Trace failed to activate. World is null."));
Completed.Broadcast(TArray<FHitResult>());
SetReadyToDestroy();
return;
}
const ECollisionChannel CollisionChannel = UEngineTypes::ConvertToCollisionChannel(TraceChannel);
const EAsyncTraceType AsyncType = bMultiTrace ? EAsyncTraceType::Multi : EAsyncTraceType::Single;
FTraceDelegate TraceDelegate;
TraceDelegate.BindUObject(this, &UDirectiveUtilTask_AsyncTrace::OnTraceComplete);
FCollisionQueryParams Params(FName(TEXT("DirectiveUtilAsyncTrace")), false);
if (Shape == EDirectiveUtilTraceShape::Line)
{
World->AsyncLineTraceByChannel(AsyncType, Start, End, CollisionChannel, Params, FCollisionResponseParams::DefaultResponseParam, &TraceDelegate);
}
else
{
FCollisionShape CollisionShape;
switch (Shape)
{
case EDirectiveUtilTraceShape::Sphere:
CollisionShape = FCollisionShape::MakeSphere(Radius);
break;
case EDirectiveUtilTraceShape::Box:
CollisionShape = FCollisionShape::MakeBox(HalfSize);
break;
case EDirectiveUtilTraceShape::Capsule:
CollisionShape = FCollisionShape::MakeCapsule(Radius, HalfHeight);
break;
default:
break;
}
World->AsyncSweepByChannel(AsyncType, Start, End, Orientation, CollisionChannel, CollisionShape, Params, FCollisionResponseParams::DefaultResponseParam, &TraceDelegate);
}
}
void UDirectiveUtilTask_AsyncTrace::OnTraceComplete(const FTraceHandle& Handle, FTraceDatum& Datum)
{
Completed.Broadcast(Datum.OutHits);
SetReadyToDestroy();
}

View File

@@ -0,0 +1,57 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Tasks/DirectiveUtilTask_Delay.h"
#include "DirectiveUtilLogChannels.h"
#include "Engine/Engine.h"
#include "Engine/World.h"
#include "TimerManager.h"
UDirectiveUtilTask_Delay* UDirectiveUtilTask_Delay::CancellableDelay(UObject* WorldContextObject, const float Duration)
{
UDirectiveUtilTask_Delay* Action = NewObject<UDirectiveUtilTask_Delay>();
Action->WorldContextObject = WorldContextObject;
Action->Duration = Duration;
if (WorldContextObject)
{
Action->RegisterWithGameInstance(WorldContextObject);
}
return Action;
}
void UDirectiveUtilTask_Delay::EndTask()
{
if (UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull) : nullptr)
{
World->GetTimerManager().ClearTimer(TimerHandle);
}
SetReadyToDestroy();
}
void UDirectiveUtilTask_Delay::Activate()
{
UWorld* World = WorldContextObject && GEngine
? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull)
: nullptr;
if (!World)
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Cancellable Delay failed to activate. World is null."));
SetReadyToDestroy();
return;
}
const float ClampedDuration = FMath::Max(Duration, 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);
}
void UDirectiveUtilTask_Delay::OnDelayComplete()
{
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Cancellable Delay completed."));
Completed.Broadcast();
SetReadyToDestroy();
}

View File

@@ -0,0 +1,280 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#include "Tasks/DirectiveUtilTask_MoveToLocation.h"
#include "DirectiveUtilLogChannels.h"
#include "Blueprint/AIBlueprintHelperLibrary.h"
#include "Engine/World.h"
#include "DrawDebugHelpers.h"
#include "Navigation/PathFollowingComponent.h"
#include "TimerManager.h"
UDirectiveUtilTask_MoveToLocation* UDirectiveUtilTask_MoveToLocation::MoveToLocation(
UObject* WorldContextObject,
AController* Controller,
const FVector Destination,
const float AcceptanceRadius,
const bool bCheckStuckMovement,
const float StuckThreshold,
const bool bDebugLineTrace)
{
UDirectiveUtilTask_MoveToLocation* Action = NewObject<UDirectiveUtilTask_MoveToLocation>();
Action->Controller = Controller;
Action->Destination = Destination;
Action->AcceptanceRadius = AcceptanceRadius;
Action->bDebugLineTrace = bDebugLineTrace;
Action->StuckThreshold = StuckThreshold;
Action->bCheckStuckMovement = bCheckStuckMovement;
if (WorldContextObject)
{
Action->RegisterWithGameInstance(WorldContextObject);
}
return Action;
}
void UDirectiveUtilTask_MoveToLocation::EndTask()
{
ExecuteCompleted(false);
}
void UDirectiveUtilTask_MoveToLocation::Activate()
{
if(!Controller || !Controller->GetPawn())
{
ExecuteCompleted(false);
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller or pawn has been destroyed while moving to location. Aborting."));
return;
}
StartLocation = Controller->GetPawn()->GetActorLocation();
LastCheckedLocation = StartLocation;
CurrentLocation = StartLocation;
Controller->GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &UDirectiveUtilTask_MoveToLocation::CheckMoveToLocation, 0.1f, true);
if (bCheckStuckMovement)
{
Controller->GetWorld()->GetTimerManager().SetTimer(StuckTimerHandle, this, &UDirectiveUtilTask_MoveToLocation::CheckStuckMovement, 3.f, true);
}
UAIBlueprintHelperLibrary::SimpleMoveToLocation(Controller, Destination);
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Moving controller to location (%s)."), *Destination.ToString());
if (bDebugLineTrace)
{
DrawDebugLine(
Controller->GetWorld(),
Destination + FVector(0, 0, 100),
Destination,
FColor::Green,
false,
5.0f,
0,
1.0f
);
}
}
void UDirectiveUtilTask_MoveToLocation::CheckMoveToLocation()
{
if(!Controller || !Controller->GetPawn())
{
ExecuteCompleted(false);
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller or pawn has been destroyed while moving to location. Aborting."));
return;
}
CurrentLocation = Controller->GetPawn()->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)
{
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Controller has moved to location."));
ExecuteCompleted(true);
return;
}
const UPathFollowingComponent* PathFollowing = Controller->FindComponentByClass<UPathFollowingComponent>();
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);
}
}
void UDirectiveUtilTask_MoveToLocation::CheckStuckMovement()
{
if(!Controller || !Controller->GetPawn())
{
ExecuteCompleted(false);
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller or pawn has been destroyed while moving to location. Aborting."));
return;
}
if (FVector::Dist(CurrentLocation, LastCheckedLocation) < StuckThreshold)
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller is stuck while moving to location. Aborting"));
ExecuteCompleted(false);
}
LastCheckedLocation = CurrentLocation;
}
void UDirectiveUtilTask_MoveToLocation::ExecuteCompleted(const bool bSuccess)
{
if (bHasCompleted)
{
return;
}
bHasCompleted = true;
UE_LOG(LogDirectiveUtil, Log, TEXT("Movement to location completed. Success: %s."), bSuccess ? TEXT("true") : TEXT("false"));
if (Controller)
{
Controller->GetWorld()->GetTimerManager().ClearTimer(TimerHandle);
Controller->GetWorld()->GetTimerManager().ClearTimer(StuckTimerHandle);
}
Completed.Broadcast(bSuccess);
Controller = nullptr;
Destination = FVector::ZeroVector;
SetReadyToDestroy();
}
UDirectiveUtilTask_MoveToActor* UDirectiveUtilTask_MoveToActor::MoveToActor(
UObject* WorldContextObject,
AController* Controller,
AActor* Goal,
const float AcceptanceRadius,
const bool bCheckStuckMovement,
const float StuckThreshold)
{
UDirectiveUtilTask_MoveToActor* Action = NewObject<UDirectiveUtilTask_MoveToActor>();
Action->Controller = Controller;
Action->Goal = Goal;
Action->AcceptanceRadius = AcceptanceRadius;
Action->StuckThreshold = StuckThreshold;
Action->bCheckStuckMovement = bCheckStuckMovement;
if (WorldContextObject)
{
Action->RegisterWithGameInstance(WorldContextObject);
}
return Action;
}
void UDirectiveUtilTask_MoveToActor::EndTask()
{
ExecuteCompleted(false);
}
void UDirectiveUtilTask_MoveToActor::Activate()
{
if(!Controller || !Controller->GetPawn() || !IsValid(Goal))
{
ExecuteCompleted(false);
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller, pawn, or goal has been destroyed while moving to actor. Aborting."));
return;
}
StartLocation = Controller->GetPawn()->GetActorLocation();
LastCheckedLocation = StartLocation;
CurrentLocation = StartLocation;
Controller->GetWorld()->GetTimerManager().SetTimer(TimerHandle, this, &UDirectiveUtilTask_MoveToActor::CheckMoveToActor, 0.1f, true);
if (bCheckStuckMovement)
{
Controller->GetWorld()->GetTimerManager().SetTimer(StuckTimerHandle, this, &UDirectiveUtilTask_MoveToActor::CheckStuckMovement, 3.f, true);
}
UAIBlueprintHelperLibrary::SimpleMoveToActor(Controller, Goal);
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Moving controller to actor (%s)."), *GetNameSafe(Goal));
}
void UDirectiveUtilTask_MoveToActor::CheckMoveToActor()
{
if(!Controller || !Controller->GetPawn())
{
ExecuteCompleted(false);
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller or pawn has been destroyed while moving to actor. Aborting."));
return;
}
if (!IsValid(Goal))
{
ExecuteCompleted(false);
UE_LOG(LogDirectiveUtil, Warning, TEXT("Goal actor has been destroyed while moving to actor. Aborting."));
return;
}
// The goal can move, so its location is re-read every poll.
const FVector GoalLocation = Goal->GetActorLocation();
CurrentLocation = Controller->GetPawn()->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)
{
UE_LOG(LogDirectiveUtil, Verbose, TEXT("Controller has moved to actor."));
ExecuteCompleted(true);
return;
}
const UPathFollowingComponent* PathFollowing = Controller->FindComponentByClass<UPathFollowingComponent>();
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);
}
}
void UDirectiveUtilTask_MoveToActor::CheckStuckMovement()
{
if(!Controller || !Controller->GetPawn())
{
ExecuteCompleted(false);
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller or pawn has been destroyed while moving to actor. Aborting."));
return;
}
if (FVector::Dist(CurrentLocation, LastCheckedLocation) < StuckThreshold)
{
UE_LOG(LogDirectiveUtil, Warning, TEXT("Controller is stuck while moving to actor. Aborting"));
ExecuteCompleted(false);
}
LastCheckedLocation = CurrentLocation;
}
void UDirectiveUtilTask_MoveToActor::ExecuteCompleted(const bool bSuccess)
{
if (bHasCompleted)
{
return;
}
bHasCompleted = true;
UE_LOG(LogDirectiveUtil, Log, TEXT("Movement to actor completed. Success: %s."), bSuccess ? TEXT("true") : TEXT("false"));
if (Controller)
{
Controller->GetWorld()->GetTimerManager().ClearTimer(TimerHandle);
Controller->GetWorld()->GetTimerManager().ClearTimer(StuckTimerHandle);
}
Completed.Broadcast(bSuccess);
Controller = nullptr;
Goal = nullptr;
SetReadyToDestroy();
}