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,42 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
using UnrealBuildTool;
public class DirectiveUtilitiesRuntime : ModuleRules
{
public DirectiveUtilitiesRuntime(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
"GameplayTags",
"NetCore",
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"CoreUObject",
"Engine",
"Slate",
"SlateCore",
"AIModule",
"EnhancedInput",
"ApplicationCore",
}
);
DynamicallyLoadedModuleNames.AddRange(
new string[]
{
}
);
}
}

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

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "Logging/LogMacros.h"
DIRECTIVEUTILITIESRUNTIME_API DECLARE_LOG_CATEGORY_EXTERN(LogDirectiveUtil, Log, All);
DIRECTIVEUTILITIESRUNTIME_API DECLARE_LOG_CATEGORY_EXTERN(LogDirectiveUtilEditor, Log, All);

View File

@@ -0,0 +1,15 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
class FDirectiveUtilitiesRuntimeModule : public IModuleInterface
{
public:
/** IModuleInterface implementation */
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};

View File

@@ -0,0 +1,716 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Net/Core/PushModel/PushModel.h"
#include "DirectiveUtilArrayFunctionLibrary.generated.h"
/**
* UDirectiveUtilArrayFunctionLibrary
* A collection of array utility functions that improve the usability of arrays in Blueprints.
*/
UCLASS()
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilArrayFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Returns the next index in the array.
* If the next index is greater than the last array index and bLoop is enabled, the index will loop back to the start of the array.
* Otherwise, the last index will be returned.
* Returns INDEX_NONE for an empty array.
* @param TargetArray - The array to get the next index for.
* @param Index - The current index.
* @param bLoop - If true, the index will loop back to the beginning of the array when the next index is greater than the last array index.
* Otherwise, the last index will be returned.
* @returns The next index in the array.
*/
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Next Index", CompactNodeTitle = "NEXT INDEX", ArrayParm = "TargetArray", BlueprintThreadSafe), Category="Directive Utilities|Array")
static int32 Array_NextIndex(const TArray<int32>& TargetArray, const int32 Index, const bool bLoop);
/**
* Returns the previous index in the array.
* If the previous index is less than 0 and bLoop is enabled, the index will loop back to the end of the array.
* Otherwise, 0 will be returned.
* Returns INDEX_NONE for an empty array.
* @param TargetArray - The array to get the previous index for.
* @param Index - The current index.
* @param bLoop - If the next index is greater than the last array index and bLoop is enabled, the index will loop back to the start of the array.
* Otherwise, the last index will be returned.
* @returns The previous index in the array.
*/
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Previous Index", CompactNodeTitle = "PREV INDEX", ArrayParm = "TargetArray", BlueprintThreadSafe), Category="Directive Utilities|Array")
static int32 Array_PreviousIndex(const TArray<int32>& TargetArray, const int32 Index, const bool bLoop);
/**
* Removes duplicate elements from the array in-place.
* @param TargetArray - The array to remove duplicates from.
*/
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Remove Duplicates", CompactNodeTitle = "REMOVE DUPLICATES", ArrayParm = "TargetArray"), Category="Directive Utilities|Array")
static void Array_RemoveDuplicates(const TArray<int32>& TargetArray);
/**
* Returns a copy of the first element of the array.
* @param TargetArray - The array to read from.
* @param OutItem - [out] A copy of the first element, or the default value if the array is empty.
* @returns True if the array contained a valid first element.
*/
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Get Valid First Array Item (Copy)", ArrayParm = "TargetArray", ArrayTypeDependentParams = "OutItem", BlueprintThreadSafe), Category="Directive Utilities|Array")
static bool Array_GetValidFirstItemCopy(const TArray<int32>& TargetArray, int32& OutItem);
/**
* Returns a copy of the last element of the array.
* @param TargetArray - The array to read from.
* @param OutItem - [out] A copy of the last element, or the default value if the array is empty.
* @returns True if the array contained a valid last element.
*/
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Get Valid Last Array Item (Copy)", ArrayParm = "TargetArray", ArrayTypeDependentParams = "OutItem", BlueprintThreadSafe), Category="Directive Utilities|Array")
static bool Array_GetValidLastItemCopy(const TArray<int32>& TargetArray, int32& OutItem);
/**
* Returns a copy of the element at the given index, if the index is valid.
* @param TargetArray - The array to read from.
* @param Index - The index to read.
* @param OutItem - [out] A copy of the element, or the default value if the index is invalid.
* @returns True if the index was valid.
*/
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Get Valid Array Item From Index (Copy)", ArrayParm = "TargetArray", ArrayTypeDependentParams = "OutItem", BlueprintThreadSafe), Category="Directive Utilities|Array")
static bool Array_GetValidItemFromIndexCopy(const TArray<int32>& TargetArray, const int32 Index, int32& OutItem);
/**
* Returns a copy of a random element from the array.
* @param TargetArray - The array to read from.
* @param OutItem - [out] A copy of the randomly selected element, or the default value if the array is empty.
* @param OutIndex - [out] The index of the selected element, or INDEX_NONE if the array is empty.
* @returns True if a valid element was selected.
*/
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Get Random Valid Array Item", CompactNodeTitle = "RANDOM", ArrayParm = "TargetArray", ArrayTypeDependentParams = "OutItem"), Category="Directive Utilities|Array")
static bool Array_GetRandomItem(const TArray<int32>& TargetArray, int32& OutItem, int32& OutIndex);
/**
* Returns a copy of the last element of the array without removing it.
* @param TargetArray - The array to read from.
* @param OutItem - [out] A copy of the last element, or the default value if the array is empty.
*/
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Last Value", CompactNodeTitle = "LAST", ArrayParm = "TargetArray", ArrayTypeDependentParams = "OutItem", BlueprintThreadSafe), Category="Directive Utilities|Array")
static void Array_LastValue(const TArray<int32>& TargetArray, int32& OutItem);
/**
* Removes the last element of the array and returns a copy of it.
* @param TargetArray - The array to pop from.
* @param OutItem - [out] A copy of the removed element, or the default value if the array is empty.
* @returns True if an element was removed.
*/
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Pop", ArrayParm = "TargetArray", ArrayTypeDependentParams = "OutItem"), Category="Directive Utilities|Array")
static bool Array_Pop(const TArray<int32>& TargetArray, int32& OutItem);
/**
* Removes the first element of the array and returns a copy of it.
* @param TargetArray - The array to pop from.
* @param OutItem - [out] A copy of the removed element, or the default value if the array is empty.
* @returns True if an element was removed.
*/
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Pop First", ArrayParm = "TargetArray", ArrayTypeDependentParams = "OutItem"), Category="Directive Utilities|Array")
static bool Array_PopFirst(const TArray<int32>& TargetArray, int32& OutItem);
/**
* Removes the element at the given index by swapping it with the last element (does not preserve order).
* This is O(1) but changes the position of the previously-last element.
* @param TargetArray - The array to remove from.
* @param Index - The index to remove.
* @returns True if the index was valid and an element was removed.
*/
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Remove At Swap", ArrayParm = "TargetArray"), Category="Directive Utilities|Array")
static bool Array_RemoveAtSwap(const TArray<int32>& TargetArray, const int32 Index);
/**
* Returns a copy of a contiguous range of the array. The range is clamped to the array bounds.
* @param TargetArray - The array to slice.
* @param StartIndex - The index to start copying from (clamped to [0, Length]).
* @param Count - The number of elements to copy. Values <= 0 produce an empty array.
* @param OutArray - [out] The sliced copy.
*/
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Slice", ArrayParm = "TargetArray,OutArray", ArrayTypeDependentParams = "OutArray", BlueprintThreadSafe), Category="Directive Utilities|Array")
static void Array_Slice(const TArray<int32>& TargetArray, const int32 StartIndex, const int32 Count, TArray<int32>& OutArray);
/**
* Cyclically rotates the elements of the array in place.
* @param TargetArray - The array to rotate.
* @param Shift - The number of positions to rotate. Positive rotates toward the end; negative toward the start.
*/
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Rotate", ArrayParm = "TargetArray"), Category="Directive Utilities|Array")
static void Array_Rotate(const TArray<int32>& TargetArray, const int32 Shift);
/**
* Returns a copy of the array with duplicates removed, keeping the first occurrence and preserving order.
* Unlike Remove Duplicates, this does not modify the input array.
* @param TargetArray - The array to read from.
* @param OutArray - [out] The de-duplicated copy.
*/
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Get Distinct (Copy)", ArrayParm = "TargetArray,OutArray", ArrayTypeDependentParams = "OutArray", BlueprintThreadSafe), Category="Directive Utilities|Array")
static void Array_GetDistinct(const TArray<int32>& TargetArray, TArray<int32>& OutArray);
/**
* Counts how many times an item appears in the array.
* @param TargetArray - The array to search.
* @param ItemToCount - The item to count.
* @returns The number of occurrences.
*/
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Count Occurrences", ArrayParm = "TargetArray", ArrayTypeDependentParams = "ItemToCount", AutoCreateRefTerm = "ItemToCount", BlueprintThreadSafe), Category="Directive Utilities|Array")
static int32 Array_CountOccurrences(const TArray<int32>& TargetArray, const int32& ItemToCount);
/**
* Returns the most frequently occurring element of the array (ties resolve to the earliest such element).
* @param TargetArray - The array to read from.
* @param OutItem - [out] A copy of the most common element, or the default value if the array is empty.
* @param OutCount - [out] The number of times the most common element occurs.
* @returns True if the array was non-empty.
*/
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Get Most Common", ArrayParm = "TargetArray", ArrayTypeDependentParams = "OutItem", BlueprintThreadSafe), Category="Directive Utilities|Array")
static bool Array_GetMostCommon(const TArray<int32>& TargetArray, int32& OutItem, int32& OutCount);
/*~
* Native functions that will be called by the below custom thunk layers, which read off the property address and call the appropriate native handler.
* Based off UKismetArrayLibrary implementation
~*/
static int32 GenericArray_NextIndex(const void* TargetArray, const FArrayProperty* ArrayProperty, int32 Index, bool bLoop);
static int32 GenericArray_PreviousIndex(const void* TargetArray, const FArrayProperty* ArrayProperty, int32 Index, bool bLoop);
static void GenericArray_RemoveDuplicates(void* TargetArray, const FArrayProperty* ArrayProperty);
static bool GenericArray_GetItemAtIndex(const void* TargetArray, const FArrayProperty* ArrayProperty, int32 Index, void* OutItemPtr);
static bool GenericArray_GetFirstItem(const void* TargetArray, const FArrayProperty* ArrayProperty, void* OutItemPtr);
static bool GenericArray_GetLastItem(const void* TargetArray, const FArrayProperty* ArrayProperty, void* OutItemPtr);
static bool GenericArray_GetRandomItem(const void* TargetArray, const FArrayProperty* ArrayProperty, void* OutItemPtr, int32* OutIndex);
static bool GenericArray_Pop(void* TargetArray, const FArrayProperty* ArrayProperty, void* OutItemPtr);
static bool GenericArray_PopFirst(void* TargetArray, const FArrayProperty* ArrayProperty, void* OutItemPtr);
static bool GenericArray_RemoveAtSwap(void* TargetArray, const FArrayProperty* ArrayProperty, int32 Index);
static void GenericArray_Slice(const void* TargetArray, const FArrayProperty* TargetArrayProperty, int32 StartIndex, int32 Count, void* OutArray, const FArrayProperty* OutArrayProperty);
static void GenericArray_Rotate(void* TargetArray, const FArrayProperty* ArrayProperty, int32 Shift);
static void GenericArray_GetDistinct(const void* TargetArray, const FArrayProperty* TargetArrayProperty, void* OutArray, const FArrayProperty* OutArrayProperty);
static int32 GenericArray_CountOccurrences(const void* TargetArray, const FArrayProperty* ArrayProperty, const void* ItemToCount);
static bool GenericArray_GetMostCommon(const void* TargetArray, const FArrayProperty* ArrayProperty, void* OutItemPtr, int32* OutCount);
/*~
* Custom thunk layers that read off the property address and call the appropriate native handler.
* Based off UKismetArrayLibrary implementation
~*/
DECLARE_FUNCTION(execArray_NextIndex)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
const void* ArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
P_GET_PROPERTY(FIntProperty, Index);
P_GET_UBOOL(bLoop);
P_FINISH;
P_NATIVE_BEGIN;
*static_cast<int32*>(RESULT_PARAM) = GenericArray_NextIndex(ArrayAddr, ArrayProperty, Index, bLoop);
P_NATIVE_END;
}
DECLARE_FUNCTION(execArray_PreviousIndex)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
const void* ArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
P_GET_PROPERTY(FIntProperty, Index);
P_GET_UBOOL(bLoop);
P_FINISH;
P_NATIVE_BEGIN;
*static_cast<int32*>(RESULT_PARAM) = GenericArray_PreviousIndex(ArrayAddr, ArrayProperty, Index, bLoop);
P_NATIVE_END;
}
DECLARE_FUNCTION(execArray_RemoveDuplicates)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
void* ArrayAddr = Stack.MostRecentPropertyAddress;
FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
P_FINISH;
P_NATIVE_BEGIN;
MARK_PROPERTY_DIRTY(Stack.Object, ArrayProperty);
GenericArray_RemoveDuplicates(ArrayAddr, ArrayProperty);
P_NATIVE_END;
}
DECLARE_FUNCTION(execArray_GetValidFirstItemCopy)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
const void* ArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
const FProperty* InnerProp = ArrayProperty->Inner;
const int32 PropertySize = InnerProp->GetElementSize() * InnerProp->ArrayDim;
void* StorageSpace = FMemory_Alloca(PropertySize);
InnerProp->InitializeValue(StorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(StorageSpace);
void* ItemPtr;
if (Stack.MostRecentPropertyAddress != nullptr && Stack.MostRecentProperty != nullptr
&& PropertySize == Stack.MostRecentProperty->GetElementSize() * Stack.MostRecentProperty->ArrayDim
&& (Stack.MostRecentProperty->GetClass()->IsChildOf(InnerProp->GetClass())
|| InnerProp->GetClass()->IsChildOf(Stack.MostRecentProperty->GetClass())))
{
ItemPtr = Stack.MostRecentPropertyAddress;
}
else
{
ItemPtr = StorageSpace;
}
P_FINISH;
P_NATIVE_BEGIN;
*static_cast<bool*>(RESULT_PARAM) = GenericArray_GetFirstItem(ArrayAddr, ArrayProperty, ItemPtr);
P_NATIVE_END;
InnerProp->DestroyValue(StorageSpace);
}
DECLARE_FUNCTION(execArray_GetValidLastItemCopy)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
const void* ArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
const FProperty* InnerProp = ArrayProperty->Inner;
const int32 PropertySize = InnerProp->GetElementSize() * InnerProp->ArrayDim;
void* StorageSpace = FMemory_Alloca(PropertySize);
InnerProp->InitializeValue(StorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(StorageSpace);
void* ItemPtr;
if (Stack.MostRecentPropertyAddress != nullptr && Stack.MostRecentProperty != nullptr
&& PropertySize == Stack.MostRecentProperty->GetElementSize() * Stack.MostRecentProperty->ArrayDim
&& (Stack.MostRecentProperty->GetClass()->IsChildOf(InnerProp->GetClass())
|| InnerProp->GetClass()->IsChildOf(Stack.MostRecentProperty->GetClass())))
{
ItemPtr = Stack.MostRecentPropertyAddress;
}
else
{
ItemPtr = StorageSpace;
}
P_FINISH;
P_NATIVE_BEGIN;
*static_cast<bool*>(RESULT_PARAM) = GenericArray_GetLastItem(ArrayAddr, ArrayProperty, ItemPtr);
P_NATIVE_END;
InnerProp->DestroyValue(StorageSpace);
}
DECLARE_FUNCTION(execArray_GetValidItemFromIndexCopy)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
const void* ArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
P_GET_PROPERTY(FIntProperty, Index);
const FProperty* InnerProp = ArrayProperty->Inner;
const int32 PropertySize = InnerProp->GetElementSize() * InnerProp->ArrayDim;
void* StorageSpace = FMemory_Alloca(PropertySize);
InnerProp->InitializeValue(StorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(StorageSpace);
void* ItemPtr;
if (Stack.MostRecentPropertyAddress != nullptr && Stack.MostRecentProperty != nullptr
&& PropertySize == Stack.MostRecentProperty->GetElementSize() * Stack.MostRecentProperty->ArrayDim
&& (Stack.MostRecentProperty->GetClass()->IsChildOf(InnerProp->GetClass())
|| InnerProp->GetClass()->IsChildOf(Stack.MostRecentProperty->GetClass())))
{
ItemPtr = Stack.MostRecentPropertyAddress;
}
else
{
ItemPtr = StorageSpace;
}
P_FINISH;
P_NATIVE_BEGIN;
*static_cast<bool*>(RESULT_PARAM) = GenericArray_GetItemAtIndex(ArrayAddr, ArrayProperty, Index, ItemPtr);
P_NATIVE_END;
InnerProp->DestroyValue(StorageSpace);
}
DECLARE_FUNCTION(execArray_GetRandomItem)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
const void* ArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
const FProperty* InnerProp = ArrayProperty->Inner;
const int32 PropertySize = InnerProp->GetElementSize() * InnerProp->ArrayDim;
void* StorageSpace = FMemory_Alloca(PropertySize);
InnerProp->InitializeValue(StorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(StorageSpace);
void* ItemPtr;
if (Stack.MostRecentPropertyAddress != nullptr && Stack.MostRecentProperty != nullptr
&& PropertySize == Stack.MostRecentProperty->GetElementSize() * Stack.MostRecentProperty->ArrayDim
&& (Stack.MostRecentProperty->GetClass()->IsChildOf(InnerProp->GetClass())
|| InnerProp->GetClass()->IsChildOf(Stack.MostRecentProperty->GetClass())))
{
ItemPtr = Stack.MostRecentPropertyAddress;
}
else
{
ItemPtr = StorageSpace;
}
Stack.MostRecentProperty = nullptr;
Stack.MostRecentPropertyAddress = nullptr;
Stack.StepCompiledIn<FProperty>(nullptr);
int32* OutIndex = reinterpret_cast<int32*>(Stack.MostRecentPropertyAddress);
P_FINISH;
P_NATIVE_BEGIN;
*static_cast<bool*>(RESULT_PARAM) = GenericArray_GetRandomItem(ArrayAddr, ArrayProperty, ItemPtr, OutIndex);
P_NATIVE_END;
InnerProp->DestroyValue(StorageSpace);
}
DECLARE_FUNCTION(execArray_LastValue)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
const void* ArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
const FProperty* InnerProp = ArrayProperty->Inner;
const int32 PropertySize = InnerProp->GetElementSize() * InnerProp->ArrayDim;
void* StorageSpace = FMemory_Alloca(PropertySize);
InnerProp->InitializeValue(StorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(StorageSpace);
void* ItemPtr;
if (Stack.MostRecentPropertyAddress != nullptr && Stack.MostRecentProperty != nullptr
&& PropertySize == Stack.MostRecentProperty->GetElementSize() * Stack.MostRecentProperty->ArrayDim
&& (Stack.MostRecentProperty->GetClass()->IsChildOf(InnerProp->GetClass())
|| InnerProp->GetClass()->IsChildOf(Stack.MostRecentProperty->GetClass())))
{
ItemPtr = Stack.MostRecentPropertyAddress;
}
else
{
ItemPtr = StorageSpace;
}
P_FINISH;
P_NATIVE_BEGIN;
GenericArray_GetLastItem(ArrayAddr, ArrayProperty, ItemPtr);
P_NATIVE_END;
InnerProp->DestroyValue(StorageSpace);
}
DECLARE_FUNCTION(execArray_Pop)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
void* ArrayAddr = Stack.MostRecentPropertyAddress;
FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
const FProperty* InnerProp = ArrayProperty->Inner;
const int32 PropertySize = InnerProp->GetElementSize() * InnerProp->ArrayDim;
void* StorageSpace = FMemory_Alloca(PropertySize);
InnerProp->InitializeValue(StorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(StorageSpace);
void* ItemPtr;
if (Stack.MostRecentPropertyAddress != nullptr && Stack.MostRecentProperty != nullptr
&& PropertySize == Stack.MostRecentProperty->GetElementSize() * Stack.MostRecentProperty->ArrayDim
&& (Stack.MostRecentProperty->GetClass()->IsChildOf(InnerProp->GetClass())
|| InnerProp->GetClass()->IsChildOf(Stack.MostRecentProperty->GetClass())))
{
ItemPtr = Stack.MostRecentPropertyAddress;
}
else
{
ItemPtr = StorageSpace;
}
P_FINISH;
P_NATIVE_BEGIN;
MARK_PROPERTY_DIRTY(Stack.Object, ArrayProperty);
*static_cast<bool*>(RESULT_PARAM) = GenericArray_Pop(ArrayAddr, ArrayProperty, ItemPtr);
P_NATIVE_END;
InnerProp->DestroyValue(StorageSpace);
}
DECLARE_FUNCTION(execArray_PopFirst)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
void* ArrayAddr = Stack.MostRecentPropertyAddress;
FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
const FProperty* InnerProp = ArrayProperty->Inner;
const int32 PropertySize = InnerProp->GetElementSize() * InnerProp->ArrayDim;
void* StorageSpace = FMemory_Alloca(PropertySize);
InnerProp->InitializeValue(StorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(StorageSpace);
void* ItemPtr;
if (Stack.MostRecentPropertyAddress != nullptr && Stack.MostRecentProperty != nullptr
&& PropertySize == Stack.MostRecentProperty->GetElementSize() * Stack.MostRecentProperty->ArrayDim
&& (Stack.MostRecentProperty->GetClass()->IsChildOf(InnerProp->GetClass())
|| InnerProp->GetClass()->IsChildOf(Stack.MostRecentProperty->GetClass())))
{
ItemPtr = Stack.MostRecentPropertyAddress;
}
else
{
ItemPtr = StorageSpace;
}
P_FINISH;
P_NATIVE_BEGIN;
MARK_PROPERTY_DIRTY(Stack.Object, ArrayProperty);
*static_cast<bool*>(RESULT_PARAM) = GenericArray_PopFirst(ArrayAddr, ArrayProperty, ItemPtr);
P_NATIVE_END;
InnerProp->DestroyValue(StorageSpace);
}
DECLARE_FUNCTION(execArray_RemoveAtSwap)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
void* ArrayAddr = Stack.MostRecentPropertyAddress;
FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
P_GET_PROPERTY(FIntProperty, Index);
P_FINISH;
P_NATIVE_BEGIN;
MARK_PROPERTY_DIRTY(Stack.Object, ArrayProperty);
*static_cast<bool*>(RESULT_PARAM) = GenericArray_RemoveAtSwap(ArrayAddr, ArrayProperty, Index);
P_NATIVE_END;
}
DECLARE_FUNCTION(execArray_Slice)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
const void* SourceArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* SourceArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!SourceArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
P_GET_PROPERTY(FIntProperty, StartIndex);
P_GET_PROPERTY(FIntProperty, Count);
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
void* OutArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* OutArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!OutArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
P_FINISH;
P_NATIVE_BEGIN;
GenericArray_Slice(SourceArrayAddr, SourceArrayProperty, StartIndex, Count, OutArrayAddr, OutArrayProperty);
P_NATIVE_END;
}
DECLARE_FUNCTION(execArray_Rotate)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
void* ArrayAddr = Stack.MostRecentPropertyAddress;
FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
P_GET_PROPERTY(FIntProperty, Shift);
P_FINISH;
P_NATIVE_BEGIN;
MARK_PROPERTY_DIRTY(Stack.Object, ArrayProperty);
GenericArray_Rotate(ArrayAddr, ArrayProperty, Shift);
P_NATIVE_END;
}
DECLARE_FUNCTION(execArray_GetDistinct)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
const void* SourceArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* SourceArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!SourceArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
void* OutArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* OutArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!OutArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
P_FINISH;
P_NATIVE_BEGIN;
GenericArray_GetDistinct(SourceArrayAddr, SourceArrayProperty, OutArrayAddr, OutArrayProperty);
P_NATIVE_END;
}
DECLARE_FUNCTION(execArray_CountOccurrences)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
const void* ArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
const FProperty* InnerProp = ArrayProperty->Inner;
const int32 PropertySize = InnerProp->GetElementSize() * InnerProp->ArrayDim;
void* StorageSpace = FMemory_Alloca(PropertySize);
InnerProp->InitializeValue(StorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(StorageSpace);
P_FINISH;
P_NATIVE_BEGIN;
*static_cast<int32*>(RESULT_PARAM) = GenericArray_CountOccurrences(ArrayAddr, ArrayProperty, StorageSpace);
P_NATIVE_END;
InnerProp->DestroyValue(StorageSpace);
}
DECLARE_FUNCTION(execArray_GetMostCommon)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
const void* ArrayAddr = Stack.MostRecentPropertyAddress;
const FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty)
{
Stack.bArrayContextFailed = true;
return;
}
const FProperty* InnerProp = ArrayProperty->Inner;
const int32 PropertySize = InnerProp->GetElementSize() * InnerProp->ArrayDim;
void* StorageSpace = FMemory_Alloca(PropertySize);
InnerProp->InitializeValue(StorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(StorageSpace);
void* ItemPtr;
if (Stack.MostRecentPropertyAddress != nullptr && Stack.MostRecentProperty != nullptr
&& PropertySize == Stack.MostRecentProperty->GetElementSize() * Stack.MostRecentProperty->ArrayDim
&& (Stack.MostRecentProperty->GetClass()->IsChildOf(InnerProp->GetClass())
|| InnerProp->GetClass()->IsChildOf(Stack.MostRecentProperty->GetClass())))
{
ItemPtr = Stack.MostRecentPropertyAddress;
}
else
{
ItemPtr = StorageSpace;
}
Stack.MostRecentProperty = nullptr;
Stack.MostRecentPropertyAddress = nullptr;
Stack.StepCompiledIn<FProperty>(nullptr);
int32* OutCount = reinterpret_cast<int32*>(Stack.MostRecentPropertyAddress);
P_FINISH;
P_NATIVE_BEGIN;
*static_cast<bool*>(RESULT_PARAM) = GenericArray_GetMostCommon(ArrayAddr, ArrayProperty, ItemPtr, OutCount);
P_NATIVE_END;
InnerProp->DestroyValue(StorageSpace);
}
};

View File

@@ -0,0 +1,104 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "DirectiveUtilFunctionLibrary.generated.h"
/**
* UDirectiveUtilFunctionLibrary
*
* The primary function library for the Directive Utilities plugin.
*/
UCLASS()
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Returns a list of classes derived from the given base class (not limited to Actors).
* This exposes the built-in GetDerivedClasses function to blueprints.
* @param BaseClass The base class to get the derived classes for.
* @param bRecursive Whether to include derived classes of derived classes.
* @param DerivedClasses The list of derived classes.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Utility")
static void GetChildClasses(const UClass* BaseClass, bool bRecursive, TArray<UClass*>& DerivedClasses);
/**
* Copy the provided text to the clipboard.
* @param Text - The text to copy to the clipboard.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Clipboard" )
static void CopyTextToClipboard(const FText& Text);
/**
* Copy the provided string to the clipboard.
* @param String - The string to copy to the clipboard.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Clipboard" )
static void CopyStringToClipboard(const FString& String);
/**
* Get the content from the clipboard as FText.
* @returns The text from the clipboard.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Clipboard" )
static FText GetTextFromClipboard();
/**
* Get the content from the clipboard as an FString.
* @returns The content from the clipboard as a string.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Clipboard" )
static FString GetStringFromClipboard();
/**
* Clear the clipboard.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Clipboard")
static void ClearClipboard();
/**
* Get the project version as a string.
* @returns The project version as a string.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Utility")
static FString GetProjectVersion();
/**
* Returns true if the game is running in the Unreal Editor.
* @note This will return false in packaged/standalone builds.
* @returns True if running in the editor.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Utility")
static bool IsRunningInEditor();
/**
* Checks whether a switch (e.g. "MySwitch" matching "-MySwitch") was passed on the
* process command line. Matching is case-insensitive.
* @param Switch - The switch name, without the leading dash.
* @returns True if the switch is present.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Utility")
static bool HasCommandLineSwitch(const FString& Switch);
/**
* Reads a key=value option (e.g. "MyKey" matching "-MyKey=Value") from the process
* command line. Matching is case-insensitive; quoted values are returned without the quotes.
* @param Key - The key name, without the leading dash or trailing equals sign.
* @param OutValue - [out] The option's value, or empty if the key is missing.
* @returns True if the key was present.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Utility")
static bool GetCommandLineOption(const FString& Key, FString& OutValue);
/** Core of Has Command Line Switch that checks an explicit command line. */
static bool HasCommandLineSwitch(const TCHAR* CommandLine, const FString& Switch);
/** Core of Get Command Line Option that reads from an explicit command line. */
static bool GetCommandLineOption(const TCHAR* CommandLine, const FString& Key, FString& OutValue);
};

View File

@@ -0,0 +1,124 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "GameplayTagContainer.h"
#include "DirectiveUtilGameplayTagFunctionLibrary.generated.h"
/**
* UDirectiveUtilGameplayTagFunctionLibrary
* Hierarchy navigation helpers for Gameplay Tags that the engine does not expose to Blueprints.
*/
UCLASS()
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilGameplayTagFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Returns the direct (immediate) parent of a tag, e.g. "A.B.C" returns "A.B".
* @param Tag - The tag to read.
* @returns The direct parent tag, or an invalid tag if the tag is a root or invalid.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags")
static FGameplayTag GetTagDirectParent(const FGameplayTag& Tag);
/**
* Returns all ancestor tags of a tag (its parents, grandparents, etc.), excluding the tag itself.
* e.g. "A.B.C" returns { "A.B", "A" }.
* @param Tag - The tag to read.
* @returns A container of the tag's ancestors.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags", meta = (BlueprintThreadSafe))
static FGameplayTagContainer GetTagParents(const FGameplayTag& Tag);
/**
* Returns the number of segments (hierarchy depth) of a tag, e.g. "A.B.C" returns 3.
* @param Tag - The tag to read.
* @returns The depth, or 0 for an invalid tag.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags", meta = (BlueprintThreadSafe))
static int32 GetTagDepth(const FGameplayTag& Tag);
/**
* Returns the last (leaf) segment of a tag's name, e.g. "A.B.C" returns "C".
* @param Tag - The tag to read.
* @returns The leaf segment, or an empty string for an invalid tag.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags", meta = (BlueprintThreadSafe))
static FString GetTagLeafName(const FGameplayTag& Tag);
/**
* Splits a tag's name into its individual segments, e.g. "A.B.C" returns [ "A", "B", "C" ].
* @param Tag - The tag to read.
* @returns The segments in order, or an empty array for an invalid tag.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags", meta = (BlueprintThreadSafe))
static TArray<FString> GetTagSegments(const FGameplayTag& Tag);
/**
* Returns all registered descendant tags of a tag (children, grandchildren, etc.), excluding the tag itself.
* e.g. "A" returns { "A.B", "A.B.C" } when those tags are registered.
* @param Tag - The tag to read.
* @returns A container of the tag's descendants, or an empty container for an invalid tag.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags", meta = (BlueprintThreadSafe))
static FGameplayTagContainer GetTagChildren(const FGameplayTag& Tag);
/**
* Returns only the direct (immediate) registered children of a tag, e.g. "A" returns "A.B" but not "A.B.C".
* @param Tag - The tag to read.
* @returns A container of the tag's direct children, or an empty container for an invalid tag.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags", meta = (BlueprintThreadSafe))
static FGameplayTagContainer GetTagDirectChildren(const FGameplayTag& Tag);
/**
* Returns the deepest tag that both tags share as an ancestor, e.g. "A.B.C" and "A.B.D" return "A.B".
* The result may be one of the inputs when one tag is an ancestor of the other.
* @param TagA - The first tag.
* @param TagB - The second tag.
* @returns The deepest common ancestor tag, or an invalid tag if the tags share none.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags")
static FGameplayTag GetTagCommonAncestor(const FGameplayTag& TagA, const FGameplayTag& TagB);
/**
* Truncates a tag to its first Depth segments, e.g. "A.B.C" at depth 2 returns "A.B".
* An ancestor of a registered tag is always registered itself.
* @param Tag - The tag to truncate.
* @param Depth - The number of leading segments to keep.
* @returns The truncated tag, the tag itself when Depth >= its depth, or an invalid tag when Depth < 1 or the tag is invalid.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags")
static FGameplayTag GetTagAtDepth(const FGameplayTag& Tag, int32 Depth);
/**
* Returns the registered tags that share a tag's direct parent, excluding the tag itself.
* Enumerating the siblings of a root tag (which has no parent) is not supported and returns an empty container.
* @param Tag - The tag to read.
* @returns A container of the tag's siblings, or an empty container for an invalid or root tag.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags")
static FGameplayTagContainer GetTagSiblings(const FGameplayTag& Tag);
/**
* Checks whether a tag has no registered children.
* @param Tag - The tag to test.
* @returns True if the tag is valid and has no registered descendants; false for an invalid tag.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags", meta = (BlueprintThreadSafe))
static bool IsLeafTag(const FGameplayTag& Tag);
/**
* Finds every registered gameplay tag whose name contains Substring (case-insensitive), in registry order.
* Cost scales with the size of the tag registry; intended for tooling and debug use, not per-frame calls.
* @param Substring - The text to search for. An empty string returns an empty array.
* @returns The matching tags.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|GameplayTags")
static TArray<FGameplayTag> FindRegisteredTags(const FString& Substring);
};

View File

@@ -0,0 +1,101 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Types/DirectiveUtilInputTypes.h"
#include "Types/DirectiveUtilTypes.h"
#include "DirectiveUtilInputFunctionLibrary.generated.h"
class UEnhancedInputLocalPlayerSubsystem;
/**
* UDirectiveUtilInputFunctionLibrary
* A function library that adds functionality for working with input in Unreal Engine.
*/
UCLASS()
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilInputFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Apply multiple Input Mapping Contexts.
* @param PlayerController The player controller to add the contexts to. Will attempt to get the LocalPlayer from the controller.
* @param Contexts The contexts to apply.
* @param bClearPrevious Whether to clear all previous contexts before applying the new ones.
* @returns Returns Success if the contexts were successfully applied.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Input", meta=(ExpandEnumAsExecs="ReturnValue", DefaultToSelf="PlayerController"))
static EDirectiveUtilSuccessStatus AddInputMappingContexts(
AController* PlayerController,
const TArray<FDirectiveUtilEnhancedInputContextData>& Contexts,
bool bClearPrevious);
/**
* Remove multiple Input Mapping Contexts.
* @param PlayerController The player controller to remove the contexts from. Will attempt to get the LocalPlayer from the controller.
* @param Contexts The contexts to remove.
* @returns Returns Success if the contexts were successfully removed.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Input", meta=(ExpandEnumAsExecs="ReturnValue", DefaultToSelf="PlayerController"))
static EDirectiveUtilSuccessStatus RemoveInputMappingContexts(
AController* PlayerController,
const TArray<TSoftObjectPtr<UInputMappingContext>>& Contexts);
/**
* Swap a designated Input Mapping Context with a new one.
* If the previous context is found, it will be removed and the new context will be added.
* If the previous context is not found, the new context will be added at the specified priority.
* @param PlayerController The player controller to swap the contexts on. Will attempt to get the LocalPlayer from the controller.
* @param PreviousContext The context to swap out.
* @param NewContext The context to swap in.
* @param Priority The priority to set the new context to.
* @param bUsePreviousPriority Whether to use the previous context's priority when adding the new context.
* @returns Returns Success if the contexts were successfully swapped.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Input", meta=(ExpandEnumAsExecs="ReturnValue", DefaultToSelf="PlayerController"))
static EDirectiveUtilSuccessStatus SwapInputMappingContexts(
AController* PlayerController,
TSoftObjectPtr<UInputMappingContext> PreviousContext,
TSoftObjectPtr<UInputMappingContext> NewContext,
int32 Priority,
bool bUsePreviousPriority);
/**
* Returns the Enhanced Input local player subsystem for the given controller, if available.
* @param PlayerController - The player controller; the Local Player's subsystem is retrieved.
* @returns The Enhanced Input subsystem, or null if the controller has no associated local player subsystem.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Input", meta=(DefaultToSelf="PlayerController"))
static UEnhancedInputLocalPlayerSubsystem* GetEnhancedInputSubsystem(AController* PlayerController);
/**
* Returns whether the given input mapping context is currently active on the controller.
* @param PlayerController - The player controller to query.
* @param Context - The input mapping context to check.
* @returns True if the context is currently applied.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Input", meta=(DefaultToSelf="PlayerController"))
static bool IsInputMappingContextActive(AController* PlayerController, TSoftObjectPtr<UInputMappingContext> Context);
/**
* Removes all input mapping contexts from the controller.
* @param PlayerController - The player controller to clear.
* @returns Success if the contexts were cleared.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Input", meta=(ExpandEnumAsExecs="ReturnValue", DefaultToSelf="PlayerController"))
static EDirectiveUtilSuccessStatus ClearAllInputMappingContexts(AController* PlayerController);
protected:
/**
* Attempt to get the Enhanced Input Subsystem from the provided controller.
* @param PlayerController - The player controller to get the subsystem from.
* @param EnhancedInput - The Enhanced Input Subsystem to return
* @return True if the subsystem was found.
*/
static bool TryGetEnhancedInputSubsystemFromController(
AController* PlayerController,
UEnhancedInputLocalPlayerSubsystem*& EnhancedInput);
};

View File

@@ -0,0 +1,291 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Net/Core/PushModel/PushModel.h"
#include "DirectiveUtilMapFunctionLibrary.generated.h"
/**
* UDirectiveUtilMapFunctionLibrary
* A collection of map (TMap) utility functions that improve the usability of maps in Blueprints.
*/
UCLASS()
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilMapFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Finds the value associated with Key, adding a new default-constructed entry if the key is not present.
* @param TargetMap - The map to search or add to.
* @param Key - The key to look up.
* @param Value - [out] A copy of the existing or newly added value.
*/
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Find Or Add", CompactNodeTitle = "FIND OR ADD", MapParam = "TargetMap", MapKeyParam = "Key", MapValueParam = "Value", AutoCreateRefTerm = "Key, Value"), Category="Directive Utilities|Map")
static void Map_FindOrAdd(const TMap<int32, int32>& TargetMap, const int32& Key, int32& Value);
/**
* Resets every value in the map to its default while preserving all keys.
* @param TargetMap - The map whose values will be reset.
*/
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Clear Values", CompactNodeTitle = "CLEAR VALUES", MapParam = "TargetMap"), Category="Directive Utilities|Map")
static void Map_ClearValues(const TMap<int32, int32>& TargetMap);
/**
* Gathers every key whose value is identical to Value, in map order.
* @param TargetMap - The map to search.
* @param Value - The value to look for.
* @param Keys - [out] Every key associated with Value.
*/
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Get Keys By Value", CompactNodeTitle = "KEYS BY VALUE", MapParam = "TargetMap", MapValueParam = "Value", MapKeyParam = "Keys", AutoCreateRefTerm = "Value, Keys"), Category="Directive Utilities|Map")
static void Map_GetKeysByValue(const TMap<int32, int32>& TargetMap, const int32& Value, TArray<int32>& Keys);
/**
* Checks whether any value in the map is identical to Value.
* @param TargetMap - The map to search.
* @param Value - The value to look for.
* @returns True if at least one entry holds Value.
*/
UFUNCTION(BlueprintPure, CustomThunk, meta=(DisplayName = "Has Value", CompactNodeTitle = "HAS VALUE", MapParam = "TargetMap", MapValueParam = "Value", AutoCreateRefTerm = "Value"), Category="Directive Utilities|Map")
static bool Map_HasValue(const TMap<int32, int32>& TargetMap, const int32& Value);
/**
* Removes every key in Keys from the map.
* @param TargetMap - The map to remove from.
* @param Keys - The keys to remove.
* @returns The number of entries that were actually removed.
*/
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Remove Keys", CompactNodeTitle = "REMOVE KEYS", MapParam = "TargetMap", MapKeyParam = "Keys", AutoCreateRefTerm = "Keys"), Category="Directive Utilities|Map")
static int32 Map_RemoveKeys(const TMap<int32, int32>& TargetMap, const TArray<int32>& Keys);
/**
* Copies every pair from SourceMap into TargetMap. Both maps must share key and value types.
* @param TargetMap - The map to copy into.
* @param SourceMap - The map to copy from.
* @param bOverwriteExisting - If true, keys already present in TargetMap are overwritten with SourceMap's values.
*/
UFUNCTION(BlueprintCallable, CustomThunk, meta=(DisplayName = "Append", CompactNodeTitle = "APPEND", MapParam = "TargetMap|SourceMap"), Category="Directive Utilities|Map")
static void Map_Append(const TMap<int32, int32>& TargetMap, const TMap<int32, int32>& SourceMap, bool bOverwriteExisting = true);
/*~
* Native functions called by the custom thunk layers below, which read off the property address
* and operate on the underlying map. Based off UBlueprintMapLibrary implementation.
~*/
static void GenericMap_FindOrAdd(const void* TargetMap, const FMapProperty* MapProperty, const void* KeyPtr, void* ValuePtr);
static void GenericMap_ClearValues(const void* TargetMap, const FMapProperty* MapProperty);
static void GenericMap_GetKeysByValue(const void* TargetMap, const FMapProperty* MapProperty, const void* ValuePtr, const void* TargetArray, const FArrayProperty* ArrayProperty);
static bool GenericMap_HasValue(const void* TargetMap, const FMapProperty* MapProperty, const void* ValuePtr);
static int32 GenericMap_RemoveKeys(const void* TargetMap, const FMapProperty* MapProperty, const void* TargetArray, const FArrayProperty* ArrayProperty);
static void GenericMap_Append(const void* TargetMap, const FMapProperty* TargetMapProperty, const void* SourceMap, const FMapProperty* SourceMapProperty, bool bOverwriteExisting);
/*~
* Custom thunk layers that read off the property address and call the appropriate native handler.
* Based off UBlueprintMapLibrary implementation.
~*/
DECLARE_FUNCTION(execMap_FindOrAdd)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FMapProperty>(nullptr);
void* MapAddr = Stack.MostRecentPropertyAddress;
FMapProperty* MapProperty = CastField<FMapProperty>(Stack.MostRecentProperty);
if (!MapProperty)
{
Stack.bArrayContextFailed = true;
return;
}
const FProperty* CurrKeyProp = MapProperty->KeyProp;
const int32 KeyPropertySize = CurrKeyProp->GetElementSize() * CurrKeyProp->ArrayDim;
void* KeyStorageSpace = FMemory_Alloca(KeyPropertySize);
CurrKeyProp->InitializeValue(KeyStorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(KeyStorageSpace);
const FProperty* CurrValueProp = MapProperty->ValueProp;
const int32 ValuePropertySize = CurrValueProp->GetElementSize() * CurrValueProp->ArrayDim;
void* ValueStorageSpace = FMemory_Alloca(ValuePropertySize);
CurrValueProp->InitializeValue(ValueStorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(ValueStorageSpace);
void* ItemPtr;
if (Stack.MostRecentPropertyAddress != nullptr && Stack.MostRecentProperty != nullptr
&& ValuePropertySize == Stack.MostRecentProperty->GetElementSize() * Stack.MostRecentProperty->ArrayDim
&& (Stack.MostRecentProperty->GetClass()->IsChildOf(CurrValueProp->GetClass())
|| CurrValueProp->GetClass()->IsChildOf(Stack.MostRecentProperty->GetClass())))
{
ItemPtr = Stack.MostRecentPropertyAddress;
}
else
{
ItemPtr = ValueStorageSpace;
}
P_FINISH;
P_NATIVE_BEGIN;
MARK_PROPERTY_DIRTY(Stack.Object, MapProperty);
GenericMap_FindOrAdd(MapAddr, MapProperty, KeyStorageSpace, ItemPtr);
P_NATIVE_END;
CurrValueProp->DestroyValue(ValueStorageSpace);
CurrKeyProp->DestroyValue(KeyStorageSpace);
}
DECLARE_FUNCTION(execMap_ClearValues)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FMapProperty>(nullptr);
void* MapAddr = Stack.MostRecentPropertyAddress;
FMapProperty* MapProperty = CastField<FMapProperty>(Stack.MostRecentProperty);
if (!MapProperty)
{
Stack.bArrayContextFailed = true;
return;
}
P_FINISH;
P_NATIVE_BEGIN;
MARK_PROPERTY_DIRTY(Stack.Object, MapProperty);
GenericMap_ClearValues(MapAddr, MapProperty);
P_NATIVE_END;
}
DECLARE_FUNCTION(execMap_GetKeysByValue)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FMapProperty>(nullptr);
void* MapAddr = Stack.MostRecentPropertyAddress;
FMapProperty* MapProperty = CastField<FMapProperty>(Stack.MostRecentProperty);
if (!MapProperty)
{
Stack.bArrayContextFailed = true;
return;
}
const FProperty* CurrValueProp = MapProperty->ValueProp;
const int32 ValuePropertySize = CurrValueProp->GetElementSize() * CurrValueProp->ArrayDim;
void* ValueStorageSpace = FMemory_Alloca(ValuePropertySize);
CurrValueProp->InitializeValue(ValueStorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(ValueStorageSpace);
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
void* ArrayAddr = Stack.MostRecentPropertyAddress;
FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty || !ArrayProperty->Inner->SameType(MapProperty->KeyProp))
{
Stack.bArrayContextFailed = true;
CurrValueProp->DestroyValue(ValueStorageSpace);
return;
}
P_FINISH;
P_NATIVE_BEGIN;
GenericMap_GetKeysByValue(MapAddr, MapProperty, ValueStorageSpace, ArrayAddr, ArrayProperty);
P_NATIVE_END;
CurrValueProp->DestroyValue(ValueStorageSpace);
}
DECLARE_FUNCTION(execMap_HasValue)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FMapProperty>(nullptr);
void* MapAddr = Stack.MostRecentPropertyAddress;
FMapProperty* MapProperty = CastField<FMapProperty>(Stack.MostRecentProperty);
if (!MapProperty)
{
Stack.bArrayContextFailed = true;
return;
}
const FProperty* CurrValueProp = MapProperty->ValueProp;
const int32 ValuePropertySize = CurrValueProp->GetElementSize() * CurrValueProp->ArrayDim;
void* ValueStorageSpace = FMemory_Alloca(ValuePropertySize);
CurrValueProp->InitializeValue(ValueStorageSpace);
Stack.MostRecentPropertyAddress = nullptr;
Stack.MostRecentPropertyContainer = nullptr;
Stack.StepCompiledIn<FProperty>(ValueStorageSpace);
P_FINISH;
P_NATIVE_BEGIN;
*static_cast<bool*>(RESULT_PARAM) = GenericMap_HasValue(MapAddr, MapProperty, ValueStorageSpace);
P_NATIVE_END;
CurrValueProp->DestroyValue(ValueStorageSpace);
}
DECLARE_FUNCTION(execMap_RemoveKeys)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FMapProperty>(nullptr);
void* MapAddr = Stack.MostRecentPropertyAddress;
FMapProperty* MapProperty = CastField<FMapProperty>(Stack.MostRecentProperty);
if (!MapProperty)
{
Stack.bArrayContextFailed = true;
return;
}
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FArrayProperty>(nullptr);
void* ArrayAddr = Stack.MostRecentPropertyAddress;
FArrayProperty* ArrayProperty = CastField<FArrayProperty>(Stack.MostRecentProperty);
if (!ArrayProperty || !ArrayProperty->Inner->SameType(MapProperty->KeyProp))
{
Stack.bArrayContextFailed = true;
return;
}
P_FINISH;
P_NATIVE_BEGIN;
MARK_PROPERTY_DIRTY(Stack.Object, MapProperty);
*static_cast<int32*>(RESULT_PARAM) = GenericMap_RemoveKeys(MapAddr, MapProperty, ArrayAddr, ArrayProperty);
P_NATIVE_END;
}
DECLARE_FUNCTION(execMap_Append)
{
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FMapProperty>(nullptr);
void* TargetMapAddr = Stack.MostRecentPropertyAddress;
FMapProperty* TargetMapProperty = CastField<FMapProperty>(Stack.MostRecentProperty);
if (!TargetMapProperty)
{
Stack.bArrayContextFailed = true;
return;
}
Stack.MostRecentProperty = nullptr;
Stack.StepCompiledIn<FMapProperty>(nullptr);
void* SourceMapAddr = Stack.MostRecentPropertyAddress;
FMapProperty* SourceMapProperty = CastField<FMapProperty>(Stack.MostRecentProperty);
if (!SourceMapProperty
|| !SourceMapProperty->KeyProp->SameType(TargetMapProperty->KeyProp)
|| !SourceMapProperty->ValueProp->SameType(TargetMapProperty->ValueProp))
{
Stack.bArrayContextFailed = true;
return;
}
P_GET_UBOOL(bOverwriteExisting);
P_FINISH;
P_NATIVE_BEGIN;
MARK_PROPERTY_DIRTY(Stack.Object, TargetMapProperty);
GenericMap_Append(TargetMapAddr, TargetMapProperty, SourceMapAddr, SourceMapProperty, bOverwriteExisting);
P_NATIVE_END;
}
};

View File

@@ -0,0 +1,244 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Types/DirectiveUtilMathTypes.h"
#include "DirectiveUtilMathFunctionLibrary.generated.h"
/**
* UDirectiveUtilMathFunctionLibrary
*
* Contains math functions for the Directive Utilities plugin.
*/
UCLASS()
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilMathFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Returns a perlin noise value between -1 and 1 at the given position.
* @note This exposes the built-in PerlinNoise2D function to blueprints.
* @param Position - The position to get the noise value for.
* @returns The noise value at the given position.
*/
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Random")
static float PerlinNoise2D(FVector2D Position);
/**
* Returns a perlin noise value between -1 and 1 at the given position.
* @note This exposes the built-in PerlinNoise3D function to blueprints.
* @param Position - The position to get the noise value for.
* @returns The noise value at the given position.
*/
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Random")
static float PerlinNoise3D(const FVector& Position);
/**
* Returns the angle in degrees between two vectors.
* @param A - The first vector.
* @param B - The second vector.
* @returns The angle between the two vectors in degrees.
*/
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Vector")
static float AngleBetweenVectors(const FVector& A, const FVector& B);
/**
* Applies a Back/Elastic/Bounce easing curve to a normalized alpha.
* @note These are the Penner easing curves the engine's built-in "Ease" node (EEasingFunc) does not provide.
* For Sinusoidal/Exponential/Circular/power easings, use the engine's "Ease" node instead.
* @param Alpha - The input alpha. Clamped to the [0, 1] range.
* @param EaseType - The easing curve to apply.
* @returns The eased alpha. Note that Back and Elastic curves intentionally overshoot the [0, 1] range.
*/
UFUNCTION(BlueprintPure, meta = (DisplayName = "Ease Alpha", BlueprintThreadSafe), Category = "Directive Utilities|Math|Easing")
static float EaseAlpha(float Alpha, EDirectiveUtilEaseType EaseType);
/**
* Eases a float from A to B using a Back/Elastic/Bounce easing curve.
* @param A - The start value (returned at Alpha 0).
* @param B - The target value (returned at Alpha 1).
* @param Alpha - The input alpha. Clamped to the [0, 1] range.
* @param EaseType - The easing curve to apply.
* @returns The eased value between A and B.
*/
UFUNCTION(BlueprintPure, meta = (DisplayName = "Ease (Float)", BlueprintThreadSafe), Category = "Directive Utilities|Math|Easing")
static float EaseFloat(float A, float B, float Alpha, EDirectiveUtilEaseType EaseType);
/**
* Eases a vector from A to B using a Back/Elastic/Bounce easing curve.
* @param A - The start vector (returned at Alpha 0).
* @param B - The target vector (returned at Alpha 1).
* @param Alpha - The input alpha. Clamped to the [0, 1] range.
* @param EaseType - The easing curve to apply.
* @returns The eased vector between A and B.
*/
UFUNCTION(BlueprintPure, meta = (DisplayName = "Ease (Vector)", BlueprintThreadSafe), Category = "Directive Utilities|Math|Easing")
static FVector EaseVector(const FVector& A, const FVector& B, float Alpha, EDirectiveUtilEaseType EaseType);
/**
* Eases a rotator from A to B using a Back/Elastic/Bounce easing curve (shortest-path interpolation).
* @param A - The start rotator (returned at Alpha 0).
* @param B - The target rotator (returned at Alpha 1).
* @param Alpha - The input alpha. Clamped to the [0, 1] range.
* @param EaseType - The easing curve to apply.
* @returns The eased rotator between A and B.
*/
UFUNCTION(BlueprintPure, meta = (DisplayName = "Ease (Rotator)", BlueprintThreadSafe), Category = "Directive Utilities|Math|Easing")
static FRotator EaseRotator(const FRotator& A, const FRotator& B, float Alpha, EDirectiveUtilEaseType EaseType);
/**
* Eases a color from A to B using a Back/Elastic/Bounce easing curve.
* @param A - The start color (returned at Alpha 0).
* @param B - The target color (returned at Alpha 1).
* @param Alpha - The input alpha. Clamped to the [0, 1] range.
* @param EaseType - The easing curve to apply.
* @returns The eased color between A and B.
*/
UFUNCTION(BlueprintPure, meta = (DisplayName = "Ease (Color)", BlueprintThreadSafe), Category = "Directive Utilities|Math|Easing")
static FLinearColor EaseColor(const FLinearColor& A, const FLinearColor& B, float Alpha, EDirectiveUtilEaseType EaseType);
/**
* Rounds a float to a given number of decimal places. Rounds half away from zero,
* matching "Round To Decimals (Text)".
* @note Due to floating-point representation the returned value may not display exactly;
* use "Round To Decimals (Text)" for clean display.
* @param Value - The value to round.
* @param Decimals - The number of decimal places to round to. Clamped to the [0, 10] range.
* @returns The rounded value.
*/
UFUNCTION(BlueprintPure, meta = (DisplayName = "Round To Decimals", BlueprintThreadSafe), Category = "Directive Utilities|Math|Float")
static float RoundToDecimals(float Value, int32 Decimals);
/**
* Rounds a float to a given number of decimal places and returns it as display text.
* Rounds half away from zero, matching "Round To Decimals".
* @param Value - The value to round.
* @param Decimals - The maximum number of decimal places to display. Clamped to the [0, 10] range.
* @returns The rounded value as text, formatted with the current locale.
*/
UFUNCTION(BlueprintPure, meta = (DisplayName = "Round To Decimals (Text)", BlueprintThreadSafe), Category = "Directive Utilities|Math|Float")
static FText RoundToDecimalsAsText(float Value, int32 Decimals);
/**
* Formats a byte count as a human-readable size using binary units (1024): B, KB, MB, GB, TB, PB.
* Decimals are applied only from KB up ("532 B", "1.4 MB"). Negative input formats the absolute
* value with a leading minus sign. Output is English-only.
* @param Bytes - The byte count to format.
* @param Decimals - The number of decimal places to show from KB up. Clamped to the [0, 3] range.
* @returns The formatted size text.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Math|Formatting")
static FText FormatBytes(int64 Bytes, int32 Decimals = 1);
/**
* Formats a duration in seconds as d/h/m/s units from the largest nonzero unit down, with
* two-digit padding after the first ("1h 03m 05s", "2d 04h", "45s"). With bIncludeSeconds
* false the seconds unit is dropped and sub-minute durations return "0m". Negative input gets
* a leading minus sign; non-finite input returns "0s". Output is English-only.
* @param Seconds - The duration in seconds.
* @param bIncludeSeconds - Whether to include the seconds unit.
* @returns The formatted duration text.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Math|Formatting")
static FText FormatDuration(float Seconds, bool bIncludeSeconds = true);
/**
* Formats a timestamp relative to the current local time: "just now" (under a minute),
* "N minute(s)/hour(s)/day(s) ago", or "in N ..." for future timestamps. Uses local time,
* pairing with Get Save Slot Timestamp. Output is English-only.
* @note Not pure: reads the current clock each call.
* @param Timestamp - The local timestamp to describe.
* @returns The formatted relative time text.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Math|Formatting")
static FText FormatRelativeTime(const FDateTime& Timestamp);
/**
* Returns the sum of an integer array as a 64-bit integer, so large arrays cannot overflow int32.
* @param Values - The values to sum.
* @returns The sum of the values, or 0 if the array is empty.
*/
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
static int64 GetIntArraySum(const TArray<int32>& Values);
/**
* Returns the arithmetic mean of an integer array.
* @param Values - The values to average.
* @returns The average of the values, or 0 if the array is empty.
*/
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
static float GetIntArrayAverage(const TArray<int32>& Values);
/**
* Returns the median of an integer array (computed on a sorted copy; the input is not modified).
* For an even count, returns the average of the two middle values.
* @param Values - The values to take the median of.
* @returns The median of the values, or 0 if the array is empty.
*/
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
static float GetIntArrayMedian(const TArray<int32>& Values);
/**
* Returns the population standard deviation of an integer array (divides by N, not N-1).
* @param Values - The values to measure.
* @returns The population standard deviation, or 0 if the array is empty.
*/
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
static float GetIntArrayStandardDeviation(const TArray<int32>& Values);
/**
* Returns the sum of a float array. Accumulates in double internally for precision.
* @param Values - The values to sum.
* @returns The sum of the values, or 0 if the array is empty.
*/
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
static float GetFloatArraySum(const TArray<float>& Values);
/**
* Returns the arithmetic mean of a float array. Accumulates in double internally for precision.
* @param Values - The values to average.
* @returns The average of the values, or 0 if the array is empty.
*/
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
static float GetFloatArrayAverage(const TArray<float>& Values);
/**
* Returns the median of a float array (computed on a sorted copy; the input is not modified).
* For an even count, returns the average of the two middle values.
* @param Values - The values to take the median of.
* @returns The median of the values, or 0 if the array is empty.
*/
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
static float GetFloatArrayMedian(const TArray<float>& Values);
/**
* Returns the population standard deviation of a float array (divides by N, not N-1).
* Accumulates in double internally for precision.
* @param Values - The values to measure.
* @returns The population standard deviation, or 0 if the array is empty.
*/
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe), Category = "Directive Utilities|Math|Array")
static float GetFloatArrayStandardDeviation(const TArray<float>& Values);
/**
* Returns a random index into the Weights array, where each index's probability is proportional to its weight.
* Useful for loot tables and weighted spawning. Negative weights are treated as zero.
* @param Weights - The per-index weights.
* @returns The selected index, or INDEX_NONE (-1) if the array is empty or all weights are zero.
*/
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Get Random Index From Weights"), Category = "Directive Utilities|Math|Random")
static int32 GetRandomIndexFromWeights(const TArray<float>& Weights);
/**
* Deterministic version of Get Random Index From Weights that draws from (and advances) the provided random stream.
* @param Stream - The random stream to draw from.
* @param Weights - The per-index weights. Negative weights are treated as zero.
* @returns The selected index, or INDEX_NONE (-1) if the array is empty or all weights are zero.
*/
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Get Random Index From Weights (Stream)"), Category = "Directive Utilities|Math|Random")
static int32 GetRandomIndexFromWeightsFromStream(UPARAM(ref) FRandomStream& Stream, const TArray<float>& Weights);
};

View File

@@ -0,0 +1,80 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "DirectiveUtilRegexFunctionLibrary.generated.h"
/**
* UDirectiveUtilRegexFunctionLibrary
* Exposes the engine's regular-expression matching (FRegexPattern/FRegexMatcher) to Blueprints and Python.
* @warning Complex patterns with nested quantifiers can be extremely slow on long inputs
* (catastrophic backtracking), and matching runs synchronously on the calling thread.
*/
UCLASS()
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilRegexFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Returns true if the pattern matches anywhere within the input string.
* @param Input - The string to search.
* @param Pattern - The regular expression pattern.
* @param bCaseSensitive - Whether matching is case-sensitive. Defaults to true.
* @returns True if at least one match was found.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Regex")
static bool RegexMatches(const FString& Input, const FString& Pattern, bool bCaseSensitive = true);
/**
* Finds the first match of the pattern within the input string.
* @param Input - The string to search.
* @param Pattern - The regular expression pattern.
* @param OutMatch - [out] The matched substring, or empty if no match.
* @param OutMatchStart - [out] The start index of the match, or INDEX_NONE if no match.
* @param OutMatchEnd - [out] The index just past the end of the match, or INDEX_NONE if no match.
* @param bCaseSensitive - Whether matching is case-sensitive. Defaults to true.
* @returns True if a match was found.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Regex")
static bool RegexFindFirst(const FString& Input, const FString& Pattern, FString& OutMatch, int32& OutMatchStart, int32& OutMatchEnd, bool bCaseSensitive = true);
/**
* Returns every (non-overlapping) match of the pattern within the input string.
* @note Zero-width (empty) matches are ignored.
* @param Input - The string to search.
* @param Pattern - The regular expression pattern.
* @param bCaseSensitive - Whether matching is case-sensitive. Defaults to true.
* @returns The matched substrings, in order.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Regex")
static TArray<FString> RegexFindAll(const FString& Input, const FString& Pattern, bool bCaseSensitive = true);
/**
* Replaces every match of the pattern in the input string with a literal replacement.
* @note The replacement is literal text; capture-group back-references (e.g. $1) are not expanded.
* @note Zero-width (empty) matches are ignored.
* @param Input - The string to operate on.
* @param Pattern - The regular expression pattern.
* @param Replacement - The literal text to substitute for each match.
* @param bCaseSensitive - Whether matching is case-sensitive. Defaults to true.
* @returns The string with all matches replaced.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Regex")
static FString RegexReplaceAll(const FString& Input, const FString& Pattern, const FString& Replacement, bool bCaseSensitive = true);
/**
* Returns a specific capture group from the first match of the pattern.
* @param Input - The string to search.
* @param Pattern - The regular expression pattern.
* @param GroupIndex - The capture group to retrieve. 0 is the entire match; 1+ are the parenthesized groups.
* @param OutGroup - [out] The captured substring, or empty if the group did not participate in the match.
* @param bCaseSensitive - Whether matching is case-sensitive. Defaults to true.
* @returns True if a match was found and the requested group participated in it.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Regex")
static bool RegexGetCaptureGroup(const FString& Input, const FString& Pattern, int32 GroupIndex, FString& OutGroup, bool bCaseSensitive = true);
};

View File

@@ -0,0 +1,93 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "DirectiveUtilSaveGameFunctionLibrary.generated.h"
class USaveGame;
/**
* UDirectiveUtilSaveGameFunctionLibrary
* Save-slot utilities that fill the gaps left by UGameplayStatics: enumerating slots, reading slot
* timestamps, and serializing a save object to/from an in-memory byte array. This is slot/IO QoL only,
* not a save framework: use the engine's SaveGameToSlot/LoadGameFromSlot for the actual slot I/O.
*/
UCLASS()
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilSaveGameFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Returns the names of all existing save slots in the project's default save directory.
* @note This enumerates the engine's default file-based save directory (Saved/SaveGames); it does not
* cover platform-specific save systems (e.g. console storage).
* @returns The save slot names (without extension).
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|SaveGame")
static TArray<FString> GetAllSaveSlotNames();
/**
* Returns the last-modified timestamp of a save slot, if it exists.
* @param SlotName - The save slot name.
* @param OutTimestamp - [out] The slot's last-modified time (local), or a default time if it does not exist.
* Converted from the file system's UTC timestamp to local time.
* @returns True if the slot exists.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|SaveGame")
static bool GetSaveSlotTimestamp(const FString& SlotName, FDateTime& OutTimestamp);
/**
* Serializes a save game object to an in-memory byte array (instead of a slot file).
* Useful for custom storage, networking, or cloud saves.
* @param SaveGameObject - The save game object to serialize.
* @param OutBytes - [out] The serialized bytes.
* @returns True if serialization succeeded.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|SaveGame")
static bool SaveGameToBytes(USaveGame* SaveGameObject, TArray<uint8>& OutBytes);
/**
* Deserializes a save game object from an in-memory byte array produced by Save Game To Bytes.
* @param SaveData - The serialized bytes.
* @returns The deserialized save game object, or null on failure.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|SaveGame")
static USaveGame* LoadGameFromBytes(const TArray<uint8>& SaveData);
/**
* Checks whether a save slot exists. Goes through the engine's save game system, so unlike
* enumeration it also works on platform save backends.
* @param SlotName - The save slot name.
* @param UserIndex - The platform user index the save belongs to.
* @returns True if the slot exists.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|SaveGame")
static bool DoesSaveSlotExist(const FString& SlotName, int32 UserIndex = 0);
/**
* Deletes a save slot. Goes through the engine's save game system, so unlike enumeration it
* also works on platform save backends.
* @param SlotName - The save slot name.
* @param UserIndex - The platform user index the save belongs to.
* @returns True if a save was actually deleted.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|SaveGame")
static bool DeleteSaveSlot(const FString& SlotName, int32 UserIndex = 0);
/**
* Renames a save slot by copying its data to the new name and then deleting the original.
* Fails without mutating anything unless both names are valid, the names differ, the old slot
* exists, and the new slot does not. On failure the original slot is never lost. Goes through
* the engine's save game system, so unlike enumeration it also works on platform save backends.
* @param OldSlotName - The existing save slot name.
* @param NewSlotName - The new save slot name.
* @param UserIndex - The platform user index the save belongs to.
* @returns True if the slot was renamed.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|SaveGame")
static bool RenameSaveSlot(const FString& OldSlotName, const FString& NewSlotName, int32 UserIndex = 0);
};

View File

@@ -0,0 +1,339 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Types/DirectiveUtilTypes.h"
#include "DirectiveUtilStringFunctionLibrary.generated.h"
/**
* UDirectiveUtilStringFunctionLibrary
* A collection of helpful string utility functions that improve the usability of strings in Blueprints.
*/
UCLASS()
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilStringFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Detect if the provided string contains any letters.
* @note Operates on ASCII characters only; non-ASCII letters/digits are not classified or cased.
* @param String - The string to check.
* @returns True if the string contains letters.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String" )
static bool ContainsLetters(const FString& String);
/**
* Detect if the provided string contains any numbers.
* @note Operates on ASCII characters only; non-ASCII letters/digits are not classified or cased.
* @param String - The string to check.
* @returns True if the string contains numbers.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String" )
static bool ContainsNumbers(const FString& String);
/**
* Detect if the provided string contains any spaces.
* @param String - The string to check.
* @returns True if the string contains spaces.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String" )
static bool ContainsSpaces(const FString& String);
/**
* Detect if the provided string contains any special characters.
* @note Operates on ASCII characters only; non-ASCII letters/digits are not classified or cased.
* @param String - The string to check.
* @returns True if the string contains special characters.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String" )
static bool ContainsSpecialCharacters(const FString& String);
/**
* Filter out characters types from the string.
* @note Operates on ASCII characters only; non-ASCII letters/digits are not classified or cased.
* @param String - The string to filter.
* @param bLetters - If true, filter out letters.
* @param bNumbers - If true, filter out numbers.
* @param bSpecialCharacters - If true, filter out special characters.
* @param bSpaces - If true, filter out spaces.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String" )
static FString FilterCharacters(const FString& String, const bool bLetters, const bool bNumbers, const bool bSpecialCharacters, const bool bSpaces);
/**
* Sort a string array alphabetically.
* @param StringArray - The string array to sort.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String", meta = (DeprecatedFunction, DeprecationMessage = "Use the engine's Sort String Array node (UE 5.6+) instead."))
static TArray<FString> SortStringArray(TArray<FString> StringArray);
/**
* Truncates a string to the specified length and appends a suffix.
* @param String - The string to truncate.
* @param MaxLength - The maximum length of the resulting string including the suffix. If MaxLength is smaller than the suffix length, the full suffix is still returned.
* @param Suffix - The suffix to append when truncated.
* @returns The truncated string.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString TruncateString(const FString& String, int32 MaxLength, const FString& Suffix = TEXT("..."));
/**
* Converts a string to title case, capitalizing the first letter of each word.
* @note Operates on ASCII characters only; non-ASCII letters/digits are not classified or cased.
* @param String - The string to convert.
* @returns The title-cased string.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString ToTitleCase(const FString& String);
/**
* Splits a string into its component words. Words are delimited by any non-alphanumeric
* character (which is consumed), a lower-to-upper transition ("fooBar" -> "foo", "Bar"),
* the end of an acronym ("XMLParser" -> "XML", "Parser"), or a letter/digit transition
* ("version2Beta" -> "version", "2", "Beta").
* @note Operates on ASCII characters only; non-ASCII letters/digits are not classified or cased.
* @param String - The string to split.
* @returns The words in order, or an empty array for an empty input.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static TArray<FString> SplitIntoWords(const FString& String);
/**
* Converts a string to camelCase: the first word lowercased, every following word capitalized,
* joined without separators ("my var name" -> "myVarName").
* @note Operates on ASCII characters only; non-ASCII letters/digits are not classified or cased.
* @param String - The string to convert.
* @returns The camelCased string.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString ToCamelCase(const FString& String);
/**
* Converts a string to PascalCase: every word capitalized, joined without separators
* ("my var name" -> "MyVarName").
* @note Operates on ASCII characters only; non-ASCII letters/digits are not classified or cased.
* @param String - The string to convert.
* @returns The PascalCased string.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString ToPascalCase(const FString& String);
/**
* Converts a string to snake_case: every word lowercased, joined with underscores
* ("my var name" -> "my_var_name").
* @note Operates on ASCII characters only; non-ASCII letters/digits are not classified or cased.
* @param String - The string to convert.
* @returns The snake_cased string.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString ToSnakeCase(const FString& String);
/**
* Converts a string to kebab-case: every word lowercased, joined with hyphens
* ("my var name" -> "my-var-name").
* @note Operates on ASCII characters only; non-ASCII letters/digits are not classified or cased.
* @param String - The string to convert.
* @returns The kebab-cased string.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString ToKebabCase(const FString& String);
/**
* Returns a sorted copy of the provided string array.
* @param StringArray - The array of strings to sort.
* @returns A sorted copy of the provided string array.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|String", meta = (DeprecatedFunction, DeprecationMessage = "Use the engine's Sort String Array node (UE 5.6+) instead."))
static TArray<FString> GetSortedStringArray(TArray<FString> StringArray);
/**
* Returns the Levenshtein (edit) distance between two strings: the minimum number of single-character
* insertions, deletions, or substitutions needed to turn one string into the other.
* @param A - The first string.
* @param B - The second string.
* @param bCaseSensitive - Whether the comparison is case-sensitive. Defaults to true.
* @returns The edit distance (0 means identical).
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static int32 GetLevenshteinDistance(const FString& A, const FString& B, bool bCaseSensitive = true);
/**
* Returns a normalized similarity score between two strings, from 0 (completely different) to 1 (identical),
* derived from the Levenshtein distance. Two empty strings are considered identical.
* @param A - The first string.
* @param B - The second string.
* @param bCaseSensitive - Whether the comparison is case-sensitive. Defaults to true.
* @returns The similarity in the [0, 1] range.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static float GetStringSimilarity(const FString& A, const FString& B, bool bCaseSensitive = true);
/**
* Returns true if the source string contains any of the provided search terms. Empty terms are ignored.
* @param Source - The string to search.
* @param SearchTerms - The substrings to look for.
* @param bCaseSensitive - Whether the search is case-sensitive. Defaults to true.
* @returns True if at least one non-empty term is found.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static bool ContainsAny(const FString& Source, const TArray<FString>& SearchTerms, bool bCaseSensitive = true);
/**
* Finds the earliest occurrence in the source string of any of the provided search terms.
* @param Source - The string to search.
* @param SearchTerms - The substrings to look for. Empty terms are ignored.
* @param bCaseSensitive - Whether the search is case-sensitive. Defaults to true.
* @param OutFoundIndex - [out] The index in Source of the earliest match, or INDEX_NONE if none.
* @param OutTermIndex - [out] The index into SearchTerms of the matched term, or INDEX_NONE if none.
* @returns True if any term was found.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static bool FindFirstOfAny(const FString& Source, const TArray<FString>& SearchTerms, bool bCaseSensitive, int32& OutFoundIndex, int32& OutTermIndex);
/**
* Encodes a string to Base64.
* @param Source - The string to encode.
* @returns The Base64-encoded string.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString Base64Encode(const FString& Source);
/**
* Decodes a Base64 string.
* @param Source - The Base64 string to decode.
* @param OutDecoded - [out] The decoded string, or empty on failure.
* @returns True if the input was valid Base64 and was decoded.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static bool Base64Decode(const FString& Source, FString& OutDecoded);
/**
* Encodes a string as lowercase hex. The string is converted to UTF-8 bytes first.
* @param String - The string to encode.
* @returns The lowercase hex encoding of the string's UTF-8 bytes.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString HexEncode(const FString& String);
/**
* Decodes a hex string (either case accepted) into the string its bytes spell in UTF-8.
* @param Hex - The hex string to decode.
* @param OutString - [out] The decoded string, or empty on failure.
* @returns True if the input was valid even-length hex and was decoded.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static bool HexDecode(const FString& Hex, FString& OutString);
/**
* Encodes a byte array as lowercase hex.
* @param Bytes - The bytes to encode.
* @returns The lowercase hex encoding of the bytes.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString HexEncodeBytes(const TArray<uint8>& Bytes);
/**
* Decodes a hex string (either case accepted) into a byte array.
* @param Hex - The hex string to decode.
* @param OutBytes - [out] The decoded bytes, or empty on failure.
* @returns True if the input was valid even-length hex and was decoded.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static bool HexDecodeBytes(const FString& Hex, TArray<uint8>& OutBytes);
/**
* Returns the MD5 digest of the string as 32 lowercase hex characters. The string is
* converted to UTF-8 bytes first.
* @note For integrity checks and cache keys, not for security or password storage.
* @param String - The string to hash.
* @returns The MD5 digest as lowercase hex.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString Md5HashString(const FString& String);
/**
* Returns the MD5 digest of a byte array as 32 lowercase hex characters.
* @note For integrity checks and cache keys, not for security or password storage.
* @param Bytes - The bytes to hash.
* @returns The MD5 digest as lowercase hex.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString Md5HashBytes(const TArray<uint8>& Bytes);
/**
* Returns the SHA-1 digest of the string as 40 lowercase hex characters. The string is
* converted to UTF-8 bytes first.
* @note For integrity checks and cache keys, not for security or password storage.
* @param String - The string to hash.
* @returns The SHA-1 digest as lowercase hex.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString Sha1HashString(const FString& String);
/**
* Returns the SHA-1 digest of a byte array as 40 lowercase hex characters.
* @note For integrity checks and cache keys, not for security or password storage.
* @param Bytes - The bytes to hash.
* @returns The SHA-1 digest as lowercase hex.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString Sha1HashBytes(const TArray<uint8>& Bytes);
/**
* Returns the CRC32 checksum of the string (seed 0). The string is converted to
* UTF-8 bytes first.
* @note For integrity checks and cache keys, not for security or password storage.
* @param String - The string to checksum.
* @returns The CRC32 checksum.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static int32 Crc32String(const FString& String);
/**
* Returns the CRC32 checksum of a byte array (seed 0).
* @note For integrity checks and cache keys, not for security or password storage.
* @param Bytes - The bytes to checksum.
* @returns The CRC32 checksum.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static int32 Crc32Bytes(const TArray<uint8>& Bytes);
/**
* Checks whether the string is safe to use as a bare file name: not empty, no path
* separators or relative segments, and no characters invalid in file names.
* @param String - The string to check.
* @returns True if the string is a valid bare file name.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static bool IsValidFileName(const FString& String);
/**
* Returns the string with path separators and characters invalid in file names removed
* (or replaced when a replacement character is provided). May return an empty string.
* @param String - The string to sanitize.
* @param Replacement - Optional single-character replacement for stripped characters.
* @returns The sanitized file name.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static FString SanitizeFileName(const FString& String, const FString& Replacement = TEXT(""));
/**
* Finds the entry in Candidates most similar to Input (by GetStringSimilarity).
* Ties resolve to the earliest index. Cost grows with array size and string
* lengths (Levenshtein per candidate). Avoid very large arrays per frame.
* @note bCaseSensitive defaults to false (matching user input), unlike the pairwise
* comparison functions where it defaults to true.
* @param Input - The string to match.
* @param Candidates - The candidate strings.
* @param OutSimilarity - [out] The winning similarity in [0, 1], 0 when empty.
* @param bCaseSensitive - Whether comparison is case-sensitive. Defaults to false.
* @returns The index of the best match, or -1 if Candidates is empty.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|String")
static int32 FindBestStringMatch(const FString& Input, const TArray<FString>& Candidates, float& OutSimilarity, bool bCaseSensitive = false);
};

View File

@@ -0,0 +1,26 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "DirectiveUtilTextFunctionLibrary.generated.h"
/**
* DirectiveUtilTextFunctionLibrary
* A collection of helpful text utility functions that improve the usability of text in Blueprints.
*/
UCLASS()
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTextFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* Returns true if the provided text is not empty.
* @param Text - The text to check.
*/
UFUNCTION(BlueprintPure, Category = "Directive Utilities|Text" )
static bool IsNotEmpty(const FText& Text);
};

View File

@@ -0,0 +1,181 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "UObject/SoftObjectPtr.h"
#include "DirectiveUtilTask_AsyncLoadAsset.generated.h"
struct FStreamableHandle;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAsyncLoadAssetCompleted, UObject*, LoadedAsset);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAsyncLoadClassCompleted, UClass*, LoadedClass);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAsyncLoadAssetsCompleted, const TArray<UObject*>&, LoadedAssets);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnAsyncLoadAssetsProgress, int32, LoadedCount, int32, TotalCount);
/**
* DirectiveUtilTask_AsyncLoadAsset
* Asynchronously loads a soft object reference and broadcasts the loaded asset, with a cancel option.
*/
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Load Asset"))
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_AsyncLoadAsset : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
/**
* Asynchronously loads the asset referenced by a soft object pointer.
* The Completed delegate is called with the loaded asset on success; the Failed delegate is called with null on failure. A manual Cancel() does not broadcast Failed.
*
* @param WorldContextObject The world context object.
* @param Asset The soft object reference to load.
*/
UFUNCTION(
BlueprintCallable,
meta=(
BlueprintInternalUseOnly = "true",
Category = "Directive Utilities|AssetManagement",
WorldContext = "WorldContextObject",
DisplayName = "Async Load Asset"
))
static UDirectiveUtilTask_AsyncLoadAsset* AsyncLoadAsset(UObject* WorldContextObject, const TSoftObjectPtr<UObject> Asset);
/**
* Cancels the in-progress load. The Failed delegate is not broadcast for a manual cancel.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|AssetManagement")
void Cancel();
virtual void Activate() override;
// Called when the asset has finished loading. The loaded asset is valid.
UPROPERTY(BlueprintAssignable)
FOnAsyncLoadAssetCompleted Completed;
// Called when the load failed. The loaded asset is null.
UPROPERTY(BlueprintAssignable)
FOnAsyncLoadAssetCompleted Failed;
protected:
TSoftObjectPtr<UObject> SoftAsset;
TSharedPtr<FStreamableHandle> StreamableHandle;
void OnLoaded();
};
/**
* DirectiveUtilTask_AsyncLoadClass
* Asynchronously loads a soft class reference and broadcasts the loaded class, with a cancel option.
*/
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Load Class"))
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_AsyncLoadClass : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
/**
* Asynchronously loads the class referenced by a soft class pointer.
* The Completed delegate is called with the loaded class on success; the Failed delegate is called with null on failure. A manual Cancel() does not broadcast Failed.
*
* @param WorldContextObject The world context object.
* @param AssetClass The soft class reference to load.
*/
UFUNCTION(
BlueprintCallable,
meta=(
BlueprintInternalUseOnly = "true",
Category = "Directive Utilities|AssetManagement",
WorldContext = "WorldContextObject",
DisplayName = "Async Load Class"
))
static UDirectiveUtilTask_AsyncLoadClass* AsyncLoadClass(UObject* WorldContextObject, const TSoftClassPtr<UObject> AssetClass);
/**
* Cancels the in-progress load. The Failed delegate is not broadcast for a manual cancel.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|AssetManagement")
void Cancel();
virtual void Activate() override;
// Called when the class has finished loading. The loaded class is valid.
UPROPERTY(BlueprintAssignable)
FOnAsyncLoadClassCompleted Completed;
// Called when the load failed. The loaded class is null.
UPROPERTY(BlueprintAssignable)
FOnAsyncLoadClassCompleted Failed;
protected:
TSoftClassPtr<UObject> SoftClass;
TSharedPtr<FStreamableHandle> StreamableHandle;
void OnLoaded();
};
/**
* DirectiveUtilTask_AsyncLoadAssets
* Asynchronously loads a batch of soft object references in a single request and broadcasts the
* loaded assets, with progress updates and a cancel option.
*/
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Load Assets"))
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_AsyncLoadAssets : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
/**
* Asynchronously loads the assets referenced by an array of soft object pointers in a single request.
* The Completed delegate is called exactly once with the loaded assets in input order; entries that were
* unset or failed to resolve are null. An empty input array completes immediately with an empty array.
* A manual Cancel() does not broadcast Completed.
*
* @param WorldContextObject The world context object.
* @param Assets The soft object references to load.
*/
UFUNCTION(
BlueprintCallable,
meta=(
BlueprintInternalUseOnly = "true",
Category = "Directive Utilities|AssetManagement",
WorldContext = "WorldContextObject",
DisplayName = "Async Load Assets"
))
static UDirectiveUtilTask_AsyncLoadAssets* AsyncLoadAssets(UObject* WorldContextObject, const TArray<TSoftObjectPtr<UObject>>& Assets);
/**
* Cancels the in-progress load. The Completed delegate is not broadcast for a manual cancel.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|AssetManagement")
void Cancel();
virtual void Activate() override;
// Called exactly once when the batch has finished loading. The assets are in input order with null
// entries for references that were unset or failed to resolve, and are only guaranteed alive during
// this broadcast.
UPROPERTY(BlueprintAssignable)
FOnAsyncLoadAssetsCompleted Completed;
// Called as assets arrive, with the number loaded so far and the total requested. Not called when
// the request finishes before the first update (e.g. all assets were already in memory).
UPROPERTY(BlueprintAssignable)
FOnAsyncLoadAssetsProgress Progress;
protected:
TArray<TSoftObjectPtr<UObject>> SoftAssets;
TSharedPtr<FStreamableHandle> StreamableHandle;
/* Whether the task has already completed, guarding against a second broadcast. */
bool bHasCompleted = false;
void OnLoaded();
void OnUpdate(TSharedRef<FStreamableHandle> Handle);
};

View File

@@ -0,0 +1,136 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "Engine/EngineTypes.h"
#include "Engine/HitResult.h"
#include "DirectiveUtilTask_AsyncTrace.generated.h"
struct FTraceHandle;
struct FTraceDatum;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAsyncTraceCompleted, const TArray<FHitResult>&, Hits);
/** The collision shape used by an async trace. */
enum class EDirectiveUtilTraceShape : uint8
{
Line,
Sphere,
Box,
Capsule,
};
/**
* DirectiveUtilTask_AsyncTrace
* Queues a collision trace and broadcasts the hit results on the next tick.
* The trace cannot be cancelled.
*/
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Trace"))
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_AsyncTrace : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
/**
* Performs an asynchronous line trace against the given trace channel.
* @param WorldContextObject The world context object.
* @param Start The start of the trace.
* @param End The end of the trace.
* @param TraceChannel The trace channel to test against.
* @param bMultiTrace If true, returns all hits up to and including the first blocking hit; otherwise returns only the first blocking hit.
*/
UFUNCTION(
BlueprintCallable,
meta=(
BlueprintInternalUseOnly = "true",
Category = "Directive Utilities|Collision",
WorldContext = "WorldContextObject",
DisplayName = "Async Line Trace By Channel"
))
static UDirectiveUtilTask_AsyncTrace* AsyncLineTraceByChannel(UObject* WorldContextObject, const FVector Start, const FVector End, const ETraceTypeQuery TraceChannel, const bool bMultiTrace = false);
/**
* Performs an asynchronous sphere sweep against the given trace channel.
* @param WorldContextObject The world context object.
* @param Start The start of the sweep.
* @param End The end of the sweep.
* @param Radius The radius of the sphere.
* @param TraceChannel The trace channel to test against.
* @param bMultiTrace If true, returns all hits up to and including the first blocking hit; otherwise returns only the first blocking hit.
*/
UFUNCTION(
BlueprintCallable,
meta=(
BlueprintInternalUseOnly = "true",
Category = "Directive Utilities|Collision",
WorldContext = "WorldContextObject",
DisplayName = "Async Sphere Trace By Channel"
))
static UDirectiveUtilTask_AsyncTrace* AsyncSphereTraceByChannel(UObject* WorldContextObject, const FVector Start, const FVector End, const float Radius, const ETraceTypeQuery TraceChannel, const bool bMultiTrace = false);
/**
* Performs an asynchronous box sweep against the given trace channel.
* @param WorldContextObject The world context object.
* @param Start The start of the sweep.
* @param End The end of the sweep.
* @param HalfSize The half-extents of the box.
* @param Orientation The orientation of the box.
* @param TraceChannel The trace channel to test against.
* @param bMultiTrace If true, returns all hits up to and including the first blocking hit; otherwise returns only the first blocking hit.
*/
UFUNCTION(
BlueprintCallable,
meta=(
BlueprintInternalUseOnly = "true",
Category = "Directive Utilities|Collision",
WorldContext = "WorldContextObject",
DisplayName = "Async Box Trace By Channel"
))
static UDirectiveUtilTask_AsyncTrace* AsyncBoxTraceByChannel(UObject* WorldContextObject, const FVector Start, const FVector End, const FVector HalfSize, const FRotator Orientation, const ETraceTypeQuery TraceChannel, const bool bMultiTrace = false);
/**
* Performs an asynchronous capsule sweep against the given trace channel.
* @param WorldContextObject The world context object.
* @param Start The start of the sweep.
* @param End The end of the sweep.
* @param Radius The radius of the capsule.
* @param HalfHeight The half-height of the capsule (including the radius).
* @param TraceChannel The trace channel to test against.
* @param bMultiTrace If true, returns all hits up to and including the first blocking hit; otherwise returns only the first blocking hit.
*/
UFUNCTION(
BlueprintCallable,
meta=(
BlueprintInternalUseOnly = "true",
Category = "Directive Utilities|Collision",
WorldContext = "WorldContextObject",
DisplayName = "Async Capsule Trace By Channel"
))
static UDirectiveUtilTask_AsyncTrace* AsyncCapsuleTraceByChannel(UObject* WorldContextObject, const FVector Start, const FVector End, const float Radius, const float HalfHeight, const ETraceTypeQuery TraceChannel, const bool bMultiTrace = false);
virtual void Activate() override;
// Called when the trace has completed. Empty if nothing was hit.
UPROPERTY(BlueprintAssignable)
FOnAsyncTraceCompleted Completed;
protected:
UPROPERTY()
TObjectPtr<UObject> WorldContextObject;
FVector Start = FVector::ZeroVector;
FVector End = FVector::ZeroVector;
ETraceTypeQuery TraceChannel = ETraceTypeQuery::TraceTypeQuery1;
bool bMultiTrace = false;
EDirectiveUtilTraceShape Shape = EDirectiveUtilTraceShape::Line;
float Radius = 0.0f;
float HalfHeight = 0.0f;
FVector HalfSize = FVector::ZeroVector;
FQuat Orientation = FQuat::Identity;
void OnTraceComplete(const FTraceHandle& Handle, FTraceDatum& Datum);
};

View File

@@ -0,0 +1,61 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "Engine/TimerHandle.h"
#include "DirectiveUtilTask_Delay.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnDelayCompleted);
/**
* DirectiveUtilTask_Delay
* A cancellable delay that can be ended early by calling EndTask.
*/
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Cancellable Delay"))
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_Delay : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
/**
* Starts a cancellable delay.
* When the delay has completed, the Completed delegate is called.
* Call EndTask to cancel the delay before it completes.
*
* @param WorldContextObject The world context object.
* @param Duration The duration of the delay in seconds.
*/
UFUNCTION(
BlueprintCallable,
meta=(
BlueprintInternalUseOnly = "true",
Category = "Directive Utilities|FlowControl",
WorldContext = "WorldContextObject",
DisplayName = "Cancellable Delay"
))
static UDirectiveUtilTask_Delay* CancellableDelay(UObject* WorldContextObject, float Duration);
/**
* Ends the delay early.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|FlowControl")
void EndTask();
virtual void Activate() override;
// The delegate called when the delay has completed.
UPROPERTY(BlueprintAssignable)
FOnDelayCompleted Completed;
protected:
UPROPERTY()
TObjectPtr<UObject> WorldContextObject;
float Duration = 0.0f;
FTimerHandle TimerHandle;
void OnDelayComplete();
};

View File

@@ -0,0 +1,181 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "GameFramework/Controller.h"
#include "DirectiveUtilTask_MoveToLocation.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAsyncMoveToLocation, bool, bSuccess);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAsyncMoveToActor, bool, bSuccess);
/**
* DirectiveUtilTask_MoveToLocation
* Asynchronously moves an actor to a location.
*/
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Move To Location"))
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_MoveToLocation : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
/**
* Moves the actor to the specified location.
* When the movement has succeeded or failed, the Completed delegate is called exactly once with success/failure.
* bSuccess is true only when the pawn ends within AcceptanceRadius of Destination; the task also ends
* (with the same distance test) when path-following stops for any reason.
*
* If the controller or pawn is destroyed while moving, the task will automatically end.
* If bCheckStuckMovement is enabled and the controller gets stuck while moving, the task will automatically end.
*
* @param WorldContextObject The world context object.
* @param Controller The controller to move.
* @param Destination The vector location to move to.
* @param AcceptanceRadius The radius around the destination location that is considered acceptable. Be sure to set this to a reasonable value as the controller may never reach the exact destination.
* @param bCheckStuckMovement Check if the controller gets stuck while moving.
* @param StuckThreshold The distance threshold to consider the controller stuck.
* @param bDebugLineTrace Display a line trace to the destination location for a short duration.
*/
UFUNCTION(
BlueprintCallable,
meta=(
BlueprintInternalUseOnly = "true",
Category = "Directive Utilities|Navigation",
WorldContext = "WorldContextObject",
DisplayName = "Async Move To Location",
AdvancedDisplay=6
))
static UDirectiveUtilTask_MoveToLocation* MoveToLocation(
UObject* WorldContextObject,
AController* Controller,
FVector Destination,
float AcceptanceRadius = 100.0f,
bool bCheckStuckMovement = true,
float StuckThreshold = 1.0f,
bool bDebugLineTrace = false);
/**
* Ends the async action.
* This must be called manually when the task is no longer necessary.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Navigation")
void EndTask();
virtual void Activate() override;
// The delegate called when the movement has completed regardless of success. Fires exactly once.
UPROPERTY(BlueprintAssignable)
FOnAsyncMoveToLocation Completed;
protected:
UPROPERTY()
AController* Controller;
FVector Destination;
FVector StartLocation;
FVector CurrentLocation;
FVector LastCheckedLocation;
float AcceptanceRadius = 10.0f;
bool bCheckStuckMovement = true;
float StuckThreshold = 1.0f;
bool bDebugLineTrace;
FTimerHandle TimerHandle;
FTimerHandle StuckTimerHandle;
bool bHasCompleted = false;
void CheckMoveToLocation();
void CheckStuckMovement();
virtual void ExecuteCompleted(bool bSuccess);
};
/**
* DirectiveUtilTask_MoveToActor
* Asynchronously moves an actor to another actor.
*/
UCLASS(BlueprintType, meta=(ExposedAsyncProxy = AsyncTask, DisplayName="Async Move To Actor"))
class DIRECTIVEUTILITIESRUNTIME_API UDirectiveUtilTask_MoveToActor : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
/**
* Moves the controller's pawn to the goal actor.
* When the movement has succeeded or failed, the Completed delegate is called exactly once with success/failure.
* bSuccess is true only when the pawn ends within AcceptanceRadius of the goal actor; the task also ends
* (with the same distance test) when path-following stops for any reason. The goal's location is re-read
* every poll, so a moving goal is tracked.
*
* If the controller, pawn, or goal actor is destroyed while moving, the task will automatically end.
* If bCheckStuckMovement is enabled and the controller gets stuck while moving, the task will automatically end.
*
* @param WorldContextObject The world context object.
* @param Controller The controller to move.
* @param Goal The actor to move to.
* @param AcceptanceRadius The radius around the goal actor that is considered acceptable. Be sure to set this to a reasonable value as the controller may never reach the goal's exact location.
* @param bCheckStuckMovement Check if the controller gets stuck while moving.
* @param StuckThreshold The distance threshold to consider the controller stuck.
*/
UFUNCTION(
BlueprintCallable,
meta=(
BlueprintInternalUseOnly = "true",
Category = "Directive Utilities|Navigation",
WorldContext = "WorldContextObject",
DisplayName = "Async Move To Actor"
))
static UDirectiveUtilTask_MoveToActor* MoveToActor(
UObject* WorldContextObject,
AController* Controller,
AActor* Goal,
float AcceptanceRadius = 100.0f,
bool bCheckStuckMovement = true,
float StuckThreshold = 1.0f);
/**
* Ends the async action.
* This must be called manually when the task is no longer necessary.
*/
UFUNCTION(BlueprintCallable, Category = "Directive Utilities|Navigation")
void EndTask();
virtual void Activate() override;
// The delegate called when the movement has completed regardless of success. Fires exactly once.
UPROPERTY(BlueprintAssignable)
FOnAsyncMoveToActor Completed;
protected:
UPROPERTY()
AController* Controller;
// The cached goal actor; GC nulls it if the actor is destroyed.
UPROPERTY()
TObjectPtr<AActor> Goal;
FVector StartLocation;
FVector CurrentLocation;
FVector LastCheckedLocation;
float AcceptanceRadius = 10.0f;
bool bCheckStuckMovement = true;
float StuckThreshold = 1.0f;
FTimerHandle TimerHandle;
FTimerHandle StuckTimerHandle;
bool bHasCompleted = false;
void CheckMoveToActor();
void CheckStuckMovement();
virtual void ExecuteCompleted(bool bSuccess);
};

View File

@@ -0,0 +1,26 @@
#pragma once
#include "CoreMinimal.h"
#include "DirectiveUtilInputTypes.generated.h"
class UInputMappingContext;
/** An input mapping context and its application priority. */
USTRUCT(BlueprintType)
struct FDirectiveUtilEnhancedInputContextData
{
GENERATED_BODY()
/** The input context to be used. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
TSoftObjectPtr<UInputMappingContext> InputContext;
/** The priority of the input context. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
int32 Priority;
FDirectiveUtilEnhancedInputContextData()
: Priority(0)
{
}
};

View File

@@ -0,0 +1,23 @@
// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License.
#pragma once
#include "CoreMinimal.h"
#include "DirectiveUtilMathTypes.generated.h"
/**
* Easing curves not provided by the engine's built-in Ease node (EEasingFunc): the classic Penner Back, Elastic and Bounce curves.
*/
UENUM(BlueprintType)
enum class EDirectiveUtilEaseType : uint8
{
BackIn UMETA(DisplayName = "Back In", Tooltip="Overshoots slightly at the start before easing in."),
BackOut UMETA(DisplayName = "Back Out", Tooltip="Overshoots slightly past the end before settling."),
BackInOut UMETA(DisplayName = "Back In Out", Tooltip="Overshoots at both the start and the end."),
ElasticIn UMETA(DisplayName = "Elastic In", Tooltip="Oscillates with increasing amplitude before easing in."),
ElasticOut UMETA(DisplayName = "Elastic Out", Tooltip="Oscillates with decreasing amplitude after the end."),
ElasticInOut UMETA(DisplayName = "Elastic In Out", Tooltip="Oscillates at both the start and the end."),
BounceIn UMETA(DisplayName = "Bounce In", Tooltip="Bounces with increasing energy before easing in."),
BounceOut UMETA(DisplayName = "Bounce Out", Tooltip="Bounces with decreasing energy after the end."),
BounceInOut UMETA(DisplayName = "Bounce In Out", Tooltip="Bounces at both the start and the end."),
};

View File

@@ -0,0 +1,14 @@
#pragma once
#include "CoreMinimal.h"
#include "DirectiveUtilTypes.generated.h"
/**
* Provides a list of success types.
*/
UENUM(BlueprintType)
enum class EDirectiveUtilSuccessStatus : uint8
{
Success,
Failure,
};