Skip to content

Instantly share code, notes, and snippets.

@Solessfir
Last active February 16, 2026 14:27
Show Gist options
  • Select an option

  • Save Solessfir/eb0df57297f8a61f0c598629b0a78865 to your computer and use it in GitHub Desktop.

Select an option

Save Solessfir/eb0df57297f8a61f0c598629b0a78865 to your computer and use it in GitHub Desktop.
C++23 std::print-style Single Header Logging library for Unreal Engine 4 and 5
/**
* Copyright (c) Solessfir under MIT license
*
* #define EASY_LOG_CATEGORY LogMyGame // Optional (must be defined before include)
* #include "EasyLog.h"
*
* Usage Examples:
*
* // Basic
* LOG_DISPLAY("Value is {}", MyInt);
*
* // Extended (Key, Duration)
* LOG_DISPLAY_EX(-1, 5.f, "Value is {}", MyInt);
*
* // Positional args
* LOG_DISPLAY("Value is {1}, expected {0}", Foo, Bar); // Value is Bar, expected Foo
*
* // Formatting Specifiers
* LOG_DISPLAY("Int: {:03}", 7); // Output: Int: 007
* LOG_DISPLAY("Float: {:.2}", 3.14159); // Output: Float: 3.14
*
* // Hex and Binary
* int32 Val = 255;
* LOG_DISPLAY("Hex: {:#x}", Val); // Output: Hex: 0xff
* LOG_DISPLAY("Bin: {:#b}", Val); // Output: Bin: 0b11111111
*
* // Pointer Address
* LOG_DISPLAY("Ptr: {:#x}", this); // Output: Ptr: 0x00...
*
* // Container Support (TArray, TSet)
* TArray<float> Values = {1.11f, 2.22f};
* LOG_DISPLAY("Values: {:.1}", Values); // Output: Values: [1.1, 2.2]
*
* // UEnum and FGameplayTag Support
* LOG_DISPLAY("State: {}", EMyEnum::Walking);
* LOG_DISPLAY("Tag: {}", MyTag);
*
* // Conditional Logging
* CLOG_ERROR(Health <= 0, "Actor {} died!", this);
*
* For UE4/UE5.0/UE5.1 - add these lines to your .Target.cs:
* bOverrideBuildEnvironment = true;
* CppStandard = CppStandardVersion.Cpp17;
*/
#pragma once
#include "CoreMinimal.h"
#include "Engine/Engine.h"
#include <cstdarg>
// -------------------------------------------------------------------------
// Version detection
// -------------------------------------------------------------------------
// EASY_LOG_MODERN = 1 : UE5.2+ (source_location, concepts, UE_LOGFMT)
// EASY_LOG_MODERN = 0 : UE4/5.0/5.1 (SFINAE, UE_LOG, __FUNCTION__)
// -------------------------------------------------------------------------
#if ENGINE_MAJOR_VERSION > 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2)
#define EASY_LOG_MODERN 1
#else
#define EASY_LOG_MODERN 0
#endif
#if EASY_LOG_MODERN
#include "Logging/StructuredLog.h"
#include <source_location>
#include <concepts>
#include <cstring>
#else
#include <type_traits>
#endif
#ifndef EASY_LOG_CATEGORY
#define EASY_LOG_CATEGORY EasyLog
#endif
// Helper to expand macro before definition
#define EASY_LOG_DEFINE_CATEGORY_INTERNAL(Category) DEFINE_LOG_CATEGORY_STATIC(Category, Log, All)
EASY_LOG_DEFINE_CATEGORY_INTERNAL(EASY_LOG_CATEGORY);
#undef EASY_LOG_DEFINE_CATEGORY_INTERNAL
// ------------------------------------------------------------------------
// Public macros
// ------------------------------------------------------------------------
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
#if EASY_LOG_MODERN
#define LOG_DISPLAY(Format, ...) \
Easy::Log<ELogVerbosity::Display>(Easy::Loc(), 10.f, Format, ##__VA_ARGS__)
#define LOG_WARNING(Format, ...) \
Easy::Log<ELogVerbosity::Warning>(Easy::Loc(), 10.f, Format, ##__VA_ARGS__)
#define LOG_ERROR(Format, ...) \
Easy::Log<ELogVerbosity::Error>(Easy::Loc(), 10.f, Format, ##__VA_ARGS__)
// do { } while(0) prevents dangling-else when used as a bare if/else body
#define CLOG_DISPLAY(Condition, Format, ...) \
do { if (Condition) LOG_DISPLAY(Format, ##__VA_ARGS__); } while(0)
#define CLOG_WARNING(Condition, Format, ...) \
do { if (Condition) LOG_WARNING(Format, ##__VA_ARGS__); } while(0)
#define CLOG_ERROR(Condition, Format, ...) \
do { if (Condition) LOG_ERROR(Format, ##__VA_ARGS__); } while(0)
#define LOG_DISPLAY_EX(Key, Duration, Format, ...) \
Easy::Log<ELogVerbosity::Display>(Key, Duration, Easy::Loc(), Format, ##__VA_ARGS__)
#define LOG_WARNING_EX(Key, Duration, Format, ...) \
Easy::Log<ELogVerbosity::Warning>(Key, Duration, Easy::Loc(), Format, ##__VA_ARGS__)
#define LOG_ERROR_EX(Key, Duration, Format, ...) \
Easy::Log<ELogVerbosity::Error>(Key, Duration, Easy::Loc(), Format, ##__VA_ARGS__)
#else // Legacy (UE4 / UE5.0 / UE5.1)
#if defined(_MSC_VER)
#define EASY_FUNC_SIG __FUNCTION__
#else
#define EASY_FUNC_SIG __PRETTY_FUNCTION__ // Clang/GCC (Linux)
#endif
#define GET_LOG_LOC Easy::FormatLocation(EASY_FUNC_SIG, __LINE__)
#define LOG_KEY_HASH static_cast<int32>( \
(FCrc::MemCrc32(__FUNCTION__, sizeof(__FUNCTION__) - 1) ^ (static_cast<uint32>(__LINE__) << 15)) & 0x7FFFFFFF)
#define LOG_DISPLAY(Fmt, ...) Easy::Dispatch<ELogVerbosity::Display>(LOG_KEY_HASH, 10.f, GET_LOG_LOC, Fmt, ##__VA_ARGS__)
#define LOG_WARNING(Fmt, ...) Easy::Dispatch<ELogVerbosity::Warning>(LOG_KEY_HASH, 10.f, GET_LOG_LOC, Fmt, ##__VA_ARGS__)
#define LOG_ERROR(Fmt, ...) Easy::Dispatch<ELogVerbosity::Error> (LOG_KEY_HASH, 10.f, GET_LOG_LOC, Fmt, ##__VA_ARGS__)
// do { } while(0) prevents dangling-else when used as a bare if/else body
#define CLOG_DISPLAY(Cond, Fmt, ...) do { if (Cond) LOG_DISPLAY(Fmt, ##__VA_ARGS__); } while(0)
#define CLOG_WARNING(Cond, Fmt, ...) do { if (Cond) LOG_WARNING(Fmt, ##__VA_ARGS__); } while(0)
#define CLOG_ERROR(Cond, Fmt, ...) do { if (Cond) LOG_ERROR (Fmt, ##__VA_ARGS__); } while(0)
#define LOG_DISPLAY_EX(Key, Duration, Format, ...) Easy::Dispatch<ELogVerbosity::Display>(Key, Duration, GET_LOG_LOC, Format, ##__VA_ARGS__)
#define LOG_WARNING_EX(Key, Duration, Format, ...) Easy::Dispatch<ELogVerbosity::Warning>(Key, Duration, GET_LOG_LOC, Format, ##__VA_ARGS__)
#define LOG_ERROR_EX(Key, Duration, Format, ...) Easy::Dispatch<ELogVerbosity::Error> (Key, Duration, GET_LOG_LOC, Format, ##__VA_ARGS__)
#endif // EASY_LOG_MODERN
#else // UE_BUILD_SHIPPING || UE_BUILD_TEST
#define LOG_DISPLAY(...)
#define LOG_WARNING(...)
#define LOG_ERROR(...)
#define CLOG_DISPLAY(...)
#define CLOG_WARNING(...)
#define CLOG_ERROR(...)
#define LOG_DISPLAY_EX(...)
#define LOG_WARNING_EX(...)
#define LOG_ERROR_EX(...)
#endif
namespace Easy
{
// ------------------------------------------------------------------------
// Shared: FParsedFormat, FormatStringHelper, SafeFormatter
// Compiled into both legacy and modern paths.
// ------------------------------------------------------------------------
struct FParsedFormat
{
FString FixedString;
TArray<FString> Specifiers;
};
struct FormatStringHelper
{
static FParsedFormat Parse(const FString& InFormat)
{
if (!InFormat.Contains(TEXT("{")))
{
return { InFormat, {} };
}
FParsedFormat Result;
Result.FixedString.Reserve(InFormat.Len() + 16);
int32 ArgIndex = 0;
const int32 Len = InFormat.Len();
for (int32 i = 0; i < Len; ++i)
{
const TCHAR Char = InFormat[i];
if (Char == '{')
{
if (i + 1 < Len && InFormat[i + 1] == '{')
{
Result.FixedString.AppendChar('{'); Result.FixedString.AppendChar('{'); i++;
continue;
}
int32 CloseIdx = -1;
for (int32 j = i + 1; j < Len; ++j)
{
if (InFormat[j] == '}') { CloseIdx = j; break; }
}
if (CloseIdx != -1)
{
FString Content = InFormat.Mid(i + 1, CloseIdx - (i + 1));
FString Specifier;
int32 ColonIdx;
if (Content.FindChar(TEXT(':'), ColonIdx))
{
Specifier = Content.RightChop(ColonIdx + 1);
Specifier.TrimStartAndEndInline();
Content = Content.Left(ColonIdx);
}
if (Content.IsEmpty())
{
// Case: {} or {:spec} -> Auto-Increment Index
Result.FixedString.AppendChar('{'); Result.FixedString.AppendInt(ArgIndex); Result.FixedString.AppendChar('}');
if (Result.Specifiers.Num() <= ArgIndex) Result.Specifiers.SetNum(ArgIndex + 1);
Result.Specifiers[ArgIndex] = Specifier;
ArgIndex++;
}
else if (Content.IsNumeric())
{
// Case: {0} or {0:spec} -> Explicit Index
Result.FixedString.AppendChar('{'); Result.FixedString += Content; Result.FixedString.AppendChar('}');
const int32 ExplicitIdx = FCString::Atoi(*Content);
if (Result.Specifiers.Num() <= ExplicitIdx) Result.Specifiers.SetNum(ExplicitIdx + 1);
Result.Specifiers[ExplicitIdx] = Specifier;
}
else
{
// Unsupported: {Name} -> treat as literal text
Result.FixedString.AppendChars(&InFormat[i], CloseIdx - i + 1);
}
i = CloseIdx;
continue;
}
}
Result.FixedString.AppendChar(Char);
}
return Result;
}
};
struct SafeFormatter
{
// 1024-char buffer via UE_ARRAY_COUNT prevents silent truncation of long messages
static FString RunFormat(const TCHAR* Fmt, ...)
{
TCHAR Buffer[1024];
va_list Ap;
va_start(Ap, Fmt);
FCString::GetVarArgs(Buffer, UE_ARRAY_COUNT(Buffer), Fmt, Ap);
va_end(Ap);
return FString(Buffer);
}
static FString ToBinary(const int64 Value, const bool bUsePrefix)
{
FString Result;
uint64 UVal = static_cast<uint64>(Value);
if (UVal == 0) { Result = TEXT("0"); }
else
{
while (UVal > 0)
{
Result = (UVal & 1 ? TEXT("1") : TEXT("0")) + Result;
UVal >>= 1;
}
}
return bUsePrefix ? TEXT("0b") + Result : Result;
}
static FString FormatInt(const int64 Value, const FString& Spec)
{
if (Spec.EndsWith(TEXT("b"), ESearchCase::IgnoreCase))
{
return ToBinary(Value, Spec.Contains(TEXT("#")));
}
if (Spec.EndsWith(TEXT("x"), ESearchCase::IgnoreCase))
{
FString HexSpec = Spec;
// FString::Contains defaults to IgnoreCase but ReplaceInline defaults to CaseSensitive,
// so we must be explicit on both — otherwise uppercase 'X' specs match the Contains check but the wrong ReplaceInline fires,
// leaving printf with %#X and no 'll' length modifier (UB on int64).
const bool bUppercase = HexSpec.EndsWith(TEXT("X"), ESearchCase::CaseSensitive);
if (bUppercase) HexSpec.ReplaceInline(TEXT("X"), TEXT("llX"), ESearchCase::CaseSensitive);
else HexSpec.ReplaceInline(TEXT("x"), TEXT("llx"), ESearchCase::CaseSensitive);
const FString Fmt = TEXT("%") + HexSpec;
return RunFormat(*Fmt, Value);
}
const FString Fmt = TEXT("%") + Spec + TEXT("lld");
return RunFormat(*Fmt, Value);
}
static FString FormatFloat(const double Value, const FString& Spec)
{
const FString Fmt = TEXT("%") + Spec + TEXT("f");
return RunFormat(*Fmt, Value);
}
};
#if EASY_LOG_MODERN
// ------------------------------------------------------------------------
// Modern path: UE5.2+ (C++20, source_location, concepts, UE_LOGFMT)
// ------------------------------------------------------------------------
// Concepts
template<typename T> concept CIsStringArg = std::constructible_from<FStringFormatArg, T>;
template<typename T> concept CHasToString = requires(T t) { { t.ToString() } -> std::convertible_to<FString>; };
template<typename T> concept CIsActor = std::derived_from<std::remove_pointer_t<std::remove_cvref_t<T>>, AActor>;
template<typename T> concept CIsUObject = std::derived_from<std::remove_pointer_t<std::remove_cvref_t<T>>, UObject>;
template<typename T> concept CIsUEnum = std::is_enum_v<std::remove_cvref_t<T>> && requires { StaticEnum<std::remove_cvref_t<T>>(); };
template<typename T> concept CIsRawEnum = std::is_enum_v<std::remove_cvref_t<T>> && !CIsUEnum<T>;
template<typename T> concept CIsContainer = requires(T t) { std::begin(t); std::end(t); } && !CIsStringArg<T> && !std::is_same_v<T, FString>;
struct FSourceLoc
{
FString Function;
int32 Hash;
};
// consteval ensures std::source_location::current() is evaluated at the macro call site rather than inside the Log<> function body, giving the correct location
consteval std::source_location Loc(const std::source_location& Location = std::source_location::current())
{
return Location;
}
inline FSourceLoc ProcessLocation(const std::source_location& Location)
{
const char* FnAnsi = Location.function_name();
uint32 Hash = FCrc::MemCrc32(FnAnsi, std::strlen(FnAnsi));
Hash = (Hash ^ (Location.line() << 15)) & 0x7FFFFFFF;
FString FnName(ANSI_TO_TCHAR(FnAnsi));
int32 Index;
// Strips return type prefix (Clang/GCC on Linux return the full signature)
if (FnName.FindLastChar(TEXT(' '), Index)) FnName = FnName.RightChop(Index + 1);
if (FnName.FindChar(TEXT('('), Index)) FnName = FnName.Left(Index);
return { FString::Printf(TEXT("%s:%d"), *FnName, Location.line()), static_cast<int32>(Hash) };
}
template <typename ArgType>
FString ElementToString(const ArgType& Value, const FString& Spec = FString())
{
using RawType = std::remove_cvref_t<ArgType>;
if constexpr (std::is_same_v<RawType, std::nullptr_t>)
{
return TEXT("nullptr");
}
else if constexpr (std::is_pointer_v<RawType>)
{
if (!Spec.IsEmpty())
{
return SafeFormatter::FormatInt(reinterpret_cast<int64>(Value), Spec);
}
if constexpr (CIsUObject<RawType>)
{
// IsValid must only be called on the game thread — never dereference off-thread
if (!IsInGameThread()) return TEXT("[AsyncUObject]");
if (!IsValid(Value)) return TEXT("None");
if constexpr (CIsActor<RawType>) return Value->GetActorNameOrLabel();
return Value->GetName();
}
else if constexpr (std::is_same_v<std::remove_pointer_t<RawType>, TCHAR>)
{
return Value ? FString(Value) : TEXT("(NullString)");
}
else
{
return FString::Printf(TEXT("0x%p"), Value);
}
}
else if constexpr (std::is_floating_point_v<RawType>)
{
if (!Spec.IsEmpty()) return SafeFormatter::FormatFloat(static_cast<double>(Value), Spec);
return FString::SanitizeFloat(Value);
}
else if constexpr (std::is_integral_v<RawType> && !std::is_same_v<RawType, bool>)
{
if (!Spec.IsEmpty()) return SafeFormatter::FormatInt(static_cast<int64>(Value), Spec);
return LexToString(Value);
}
else if constexpr (CIsUEnum<RawType>)
{
if (const UEnum* EnumPtr = StaticEnum<RawType>())
{
return EnumPtr->GetNameStringByValue(static_cast<int64>(Value));
}
return LexToString(static_cast<int64>(Value));
}
else if constexpr (CIsRawEnum<RawType>)
{
// Plain C++ enum (no UENUM): format as integer
if (!Spec.IsEmpty()) return SafeFormatter::FormatInt(static_cast<int64>(Value), Spec);
return LexToString(static_cast<int64>(Value));
}
else if constexpr (CHasToString<ArgType>) return Value.ToString();
else if constexpr (CIsStringArg<ArgType>) return LexToString(Value);
else return TEXT("[?]");
}
template <typename ArgType>
FString ContainerToString(const ArgType& Container, const FString& Spec)
{
FString Result = TEXT("[");
int32 Count = 0;
constexpr int32 MaxElements = 15;
for (const auto& Elem : Container)
{
if (Count > 0) Result += TEXT(", ");
if (Count >= MaxElements) { Result += TEXT("..."); break; }
Result += ElementToString(Elem, Spec);
Count++;
}
return Result + TEXT("]");
}
inline void FillArgs(FStringFormatOrderedArguments&, const TArray<FString>&, int32) {}
template<typename FirstArgType, typename... RestArgsType>
void FillArgs(FStringFormatOrderedArguments& Args, const TArray<FString>& Specs, const int32 Index, FirstArgType&& First, RestArgsType&&... Rest)
{
FString CurrentSpec = Specs.IsValidIndex(Index) ? Specs[Index] : FString();
if constexpr (CIsContainer<std::remove_cvref_t<FirstArgType>>)
Args.Emplace(ContainerToString(First, CurrentSpec));
else
Args.Emplace(ElementToString(First, CurrentSpec));
FillArgs(Args, Specs, Index + 1, std::forward<RestArgsType>(Rest)...);
}
template <ELogVerbosity::Type Verbosity>
void LogToConsole(const FString& Message)
{
if constexpr (Verbosity == ELogVerbosity::Fatal) UE_LOGFMT(EASY_LOG_CATEGORY, Fatal, "{0}", Message);
else if constexpr (Verbosity == ELogVerbosity::Error) UE_LOGFMT(EASY_LOG_CATEGORY, Error, "{0}", Message);
else if constexpr (Verbosity == ELogVerbosity::Warning) UE_LOGFMT(EASY_LOG_CATEGORY, Warning, "{0}", Message);
else if constexpr (Verbosity == ELogVerbosity::Display) UE_LOGFMT(EASY_LOG_CATEGORY, Display, "{0}", Message);
else if constexpr (Verbosity == ELogVerbosity::Verbose) UE_LOGFMT(EASY_LOG_CATEGORY, Verbose, "{0}", Message);
else UE_LOGFMT(EASY_LOG_CATEGORY, Log, "{0}", Message);
}
// Standard LOG_* — key derived from source location hash
template <ELogVerbosity::Type Verbosity, typename... ArgsType>
void Log(const std::source_location& SourceLocation, const float Duration, const FString& Format, ArgsType&&... Arguments)
{
const auto [Function, Hash] = ProcessLocation(SourceLocation);
const auto [FixedString, Specifiers] = FormatStringHelper::Parse(Format);
FStringFormatOrderedArguments OrderedArgs;
FillArgs(OrderedArgs, Specifiers, 0, std::forward<ArgsType>(Arguments)...);
const FString UserMessage = FString::Format(*FixedString, MoveTemp(OrderedArgs));
LogToConsole<Verbosity>(FString::Printf(TEXT("%s | %s"), *UserMessage, *Function));
if (GEngine && Duration > 0.f)
{
const FColor Color = Verbosity == ELogVerbosity::Error ? FColor::Red :
Verbosity == ELogVerbosity::Warning ? FColor::Orange : FColor::White;
GEngine->AddOnScreenDebugMessage(Hash, Duration, Color, UserMessage);
}
}
// LOG_*_EX — explicit key
template <ELogVerbosity::Type Verbosity, typename... ArgsType>
void Log(const int32 Key, const float Duration, const std::source_location& SourceLocation, const FString& Format, ArgsType&&... Arguments)
{
const auto [Function, Hash] = ProcessLocation(SourceLocation);
const auto [FixedString, Specifiers] = FormatStringHelper::Parse(Format);
FStringFormatOrderedArguments OrderedArgs;
FillArgs(OrderedArgs, Specifiers, 0, std::forward<ArgsType>(Arguments)...);
const FString UserMessage = FString::Format(*FixedString, MoveTemp(OrderedArgs));
LogToConsole<Verbosity>(FString::Printf(TEXT("%s | %s"), *UserMessage, *Function));
if (GEngine && Duration > 0.f)
{
const FColor Color = Verbosity == ELogVerbosity::Error ? FColor::Red :
Verbosity == ELogVerbosity::Warning ? FColor::Orange : FColor::White;
GEngine->AddOnScreenDebugMessage(Key, Duration, Color, UserMessage);
}
}
#else
// ------------------------------------------------------------------------
// Legacy path: UE4 / UE5.0 / UE5.1 (C++17, SFINAE, UE_LOG)
// ------------------------------------------------------------------------
#define EASY_UE_LOG_EXPAND(Category, Verbosity, Format, ...) UE_LOG(Category, Verbosity, Format, ##__VA_ARGS__)
template <typename T> using Decayed = std::decay_t<T>;
// SFINAE helpers
template <typename T, typename = void> struct THasToString : std::false_type {};
template <typename T> struct THasToString<T, std::void_t<decltype(std::declval<T>().ToString())>> : std::true_type {};
template <typename T, typename = void> struct TIsContainer : std::false_type {};
template <typename T> struct TIsContainer<T, std::void_t<decltype(std::declval<T>().begin()), decltype(std::declval<T>().end())>> : std::true_type {};
inline FString FormatLocation(const char* InFunc, const int32 Line)
{
FString Result(InFunc);
// Strips parameter list and return type prefix (relevant for __PRETTY_FUNCTION__)
if (Result.Contains(TEXT("("))) Result = Result.Left(Result.Find(TEXT("(")));
int32 SpaceIdx = -1;
if (Result.FindLastChar(TEXT(' '), SpaceIdx)) Result = Result.RightChop(SpaceIdx + 1);
return FString::Printf(TEXT("%s:%d"), *Result, Line);
}
// Native FStringArg
template <typename T, typename std::enable_if_t<std::is_constructible<FStringFormatArg, T>::value && !TIsContainer<Decayed<T>>::value, int> = 0>
void Append(FStringFormatOrderedArguments& Args, const T& Val, const FString& Spec)
{
if constexpr (std::is_pointer<Decayed<T>>::value)
{
if (!Spec.IsEmpty()) { Args.Emplace(SafeFormatter::FormatInt(reinterpret_cast<int64>(Val), Spec)); return; }
if constexpr (std::is_same<typename std::remove_pointer<Decayed<T>>::type, TCHAR>::value)
{
if (Val == nullptr) { Args.Emplace(TEXT("(NullString)")); return; }
}
}
if constexpr (std::is_floating_point<Decayed<T>>::value)
{
if (!Spec.IsEmpty()) Args.Emplace(SafeFormatter::FormatFloat(static_cast<double>(Val), Spec));
else Args.Emplace(FString::SanitizeFloat(Val));
}
else if constexpr (std::is_integral<Decayed<T>>::value)
{
if (!Spec.IsEmpty()) Args.Emplace(SafeFormatter::FormatInt(static_cast<int64>(Val), Spec));
else Args.Emplace(Val);
}
else
{
Args.Emplace(Val);
}
}
// Literal nullptr
inline void Append(FStringFormatOrderedArguments& Args, std::nullptr_t, const FString&)
{
Args.Emplace(TEXT("nullptr"));
}
// ToString()
template <typename T, typename std::enable_if_t<THasToString<Decayed<T>>::value && !std::is_constructible<FStringFormatArg, T>::value, int> = 0>
void Append(FStringFormatOrderedArguments& Args, const T& Val, const FString&)
{
Args.Emplace(Val.ToString());
}
// UObject/Actor
template <typename T, typename std::enable_if_t<std::is_pointer<T>::value && std::is_base_of<UObject, std::remove_pointer_t<T>>::value, int> = 0>
void Append(FStringFormatOrderedArguments& Args, T Val, const FString& Spec)
{
if (!Spec.IsEmpty()) { Args.Emplace(SafeFormatter::FormatInt(reinterpret_cast<int64>(Val), Spec)); return; }
// IsValid must only be called on the game thread — never dereference off-thread
if (!IsInGameThread()) { Args.Emplace(TEXT("[AsyncUObject]")); return; }
Args.Emplace(IsValid(Val) ? Val->GetName() : FString(TEXT("None")));
}
// UEnum (and raw enums fall through to int via static_cast)
template <typename T, typename std::enable_if_t<std::is_enum<Decayed<T>>::value, int> = 0>
void Append(FStringFormatOrderedArguments& Args, T Val, const FString& Spec)
{
if (!Spec.IsEmpty()) { Args.Emplace(SafeFormatter::FormatInt(static_cast<int64>(Val), Spec)); return; }
const UEnum* EnumPtr = StaticEnum<Decayed<T>>();
if (EnumPtr) Args.Emplace(EnumPtr->GetNameStringByValue(static_cast<int64>(Val)));
else Args.Emplace(static_cast<int64>(Val)); // Raw enum: fall back to int value
}
// Containers
template <typename T, typename std::enable_if_t<TIsContainer<Decayed<T>>::value && !std::is_same<Decayed<T>, FString>::value, int> = 0>
void Append(FStringFormatOrderedArguments& Args, const T& Val, const FString& Spec)
{
FString Result = TEXT("[");
int32 Count = 0;
constexpr int32 MaxElements = 15;
for (const auto& Elem : Val)
{
if (Count > 0) Result += TEXT(", ");
if (Count >= MaxElements) { Result += TEXT("..."); break; }
if constexpr (std::is_floating_point<Decayed<decltype(Elem)>>::value)
{
if (!Spec.IsEmpty()) Result += SafeFormatter::FormatFloat(static_cast<double>(Elem), Spec);
else Result += FString::SanitizeFloat(Elem);
}
else if constexpr (std::is_integral<Decayed<decltype(Elem)>>::value)
{
if (!Spec.IsEmpty()) Result += SafeFormatter::FormatInt(static_cast<int64>(Elem), Spec);
else Result += LexToString(Elem);
}
else
{
Result += LexToString(Elem);
}
Count++;
}
Result += TEXT("]");
Args.Emplace(Result);
}
// Fallback
inline void Append(FStringFormatOrderedArguments& Args, ...) { Args.Emplace(TEXT("[?]")); }
inline void Fill(FStringFormatOrderedArguments&, const TArray<FString>&, int32) {}
template<typename FirstArgType, typename... RestArgsType>
void Fill(FStringFormatOrderedArguments& Args, const TArray<FString>& Specs, const int32 Index, FirstArgType&& First, RestArgsType&&... Rest)
{
FString CurrentSpec = Specs.IsValidIndex(Index) ? Specs[Index] : FString();
Append(Args, Forward<FirstArgType>(First), CurrentSpec);
Fill(Args, Specs, Index + 1, Forward<RestArgsType>(Rest)...);
}
template <ELogVerbosity::Type Verbosity, typename... ArgsType>
void Dispatch(const int32 Key, const float Duration, const FString& Location, const FString& Format, ArgsType&&... Arguments)
{
const auto [FixedString, Specifiers] = FormatStringHelper::Parse(Format);
FStringFormatOrderedArguments OrderedArgs;
Fill(OrderedArgs, Specifiers, 0, Forward<ArgsType>(Arguments)...);
const FString UserMessage = FString::Format(*FixedString, MoveTemp(OrderedArgs));
const FString FullMessage = FString::Printf(TEXT("%s | %s"), *UserMessage, *Location);
switch (Verbosity)
{
case ELogVerbosity::Fatal: EASY_UE_LOG_EXPAND(EASY_LOG_CATEGORY, Fatal, TEXT("%s"), *FullMessage); break;
case ELogVerbosity::Error: EASY_UE_LOG_EXPAND(EASY_LOG_CATEGORY, Error, TEXT("%s"), *FullMessage); break;
case ELogVerbosity::Warning: EASY_UE_LOG_EXPAND(EASY_LOG_CATEGORY, Warning, TEXT("%s"), *FullMessage); break;
case ELogVerbosity::Display: EASY_UE_LOG_EXPAND(EASY_LOG_CATEGORY, Display, TEXT("%s"), *FullMessage); break;
case ELogVerbosity::Verbose: EASY_UE_LOG_EXPAND(EASY_LOG_CATEGORY, Verbose, TEXT("%s"), *FullMessage); break;
default: EASY_UE_LOG_EXPAND(EASY_LOG_CATEGORY, Log, TEXT("%s"), *FullMessage); break;
}
if (GEngine && Duration > 0.f)
{
const FColor Color = Verbosity == ELogVerbosity::Error ? FColor::Red :
Verbosity == ELogVerbosity::Warning ? FColor::Orange : FColor::White;
GEngine->AddOnScreenDebugMessage(Key, Duration, Color, UserMessage);
}
}
#endif // EASY_LOG_MODERN
} // namespace Easy
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment