Skip to content

Instantly share code, notes, and snippets.

@Solessfir
Last active August 23, 2026 16:26
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;
*
* UE5.2+ uses the C++20 implementation when the target enables std::source_location.
* C++17 targets automatically use the legacy path.
*/
#pragma once
#include "CoreMinimal.h"
#include "Engine/Engine.h"
#include "Templates/IsUEnumClass.h"
#include <cstdio>
#include <iterator>
#include <type_traits>
#include <utility>
// -------------------------------------------------------------------------
// Version detection
// -------------------------------------------------------------------------
// EASY_LOG_MODERN = 1 : UE5.2+ with C++20 source_location (UE_LOGFMT)
// EASY_LOG_MODERN = 0 : UE4+ with C++17 (UE_LOG, __FUNCTION__)
// -------------------------------------------------------------------------
#if ENGINE_MAJOR_VERSION > 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2)
#if defined(__has_include)
#if __has_include(<version>)
#include <version>
#endif
#endif
#endif
#if (ENGINE_MAJOR_VERSION > 5 || (ENGINE_MAJOR_VERSION == 5 && ENGINE_MINOR_VERSION >= 2)) && defined(__cpp_lib_source_location) && (__cpp_lib_source_location >= 201907L)
#define EASY_LOG_MODERN 1
#else
#define EASY_LOG_MODERN 0
#endif
#if EASY_LOG_MODERN
#include "Logging/StructuredLog.h"
#include <source_location>
#include <cstring>
#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 parsing and type conversion. Both paths require only C++17.
// ------------------------------------------------------------------------
struct FFormatField
{
int32 ArgumentIndex = INDEX_NONE;
FString Specifier;
};
struct FParsedFormat
{
// Literals.Num() is always Fields.Num() + 1 for a valid parse.
TArray<FString> Literals;
TArray<FFormatField> Fields;
FString Error;
bool bIsValid = true;
};
struct FormatStringHelper
{
static bool TryParseIndex(const FString& Text, int32& OutIndex)
{
if (Text.IsEmpty()) return false;
int32 Value = 0;
for (const TCHAR Char : Text)
{
if (!TChar<TCHAR>::IsDigit(Char)) return false;
const int32 Digit = Char - TEXT('0');
if (Value > (MAX_int32 - Digit) / 10) return false;
Value = Value * 10 + Digit;
}
OutIndex = Value;
return true;
}
static bool LooksLikeInvalidIndex(const FString& Text)
{
if (Text.IsEmpty()) return false;
const TCHAR First = Text[0];
return TChar<TCHAR>::IsDigit(First) || First == TEXT('+') || First == TEXT('-') || First == TEXT('.');
}
static FParsedFormat Parse(const FString& InFormat, const int32 ArgumentCount)
{
FParsedFormat Result;
FString CurrentLiteral;
CurrentLiteral.Reserve(InFormat.Len());
int32 NextAutomaticIndex = 0;
const int32 Len = InFormat.Len();
for (int32 i = 0; i < Len; ++i)
{
const TCHAR Char = InFormat[i];
if (Char == TEXT('{'))
{
// EasyLog uses the conventional doubled-brace escape syntax.
if (i + 1 < Len && InFormat[i + 1] == TEXT('{'))
{
CurrentLiteral.AppendChar(TEXT('{'));
++i;
continue;
}
int32 CloseIndex = INDEX_NONE;
for (int32 j = i + 1; j < Len; ++j)
{
if (InFormat[j] == TEXT('}'))
{
CloseIndex = j;
break;
}
}
// An unmatched opening brace is harmless literal log text.
if (CloseIndex == INDEX_NONE)
{
CurrentLiteral.AppendChar(Char);
continue;
}
FString Content = InFormat.Mid(i + 1, CloseIndex - i - 1);
FString Specifier;
int32 ColonIndex = INDEX_NONE;
if (Content.FindChar(TEXT(':'), ColonIndex))
{
Specifier = Content.RightChop(ColonIndex + 1);
Specifier.TrimStartAndEndInline();
Content = Content.Left(ColonIndex);
}
int32 ArgumentIndex = INDEX_NONE;
bool bIsPlaceholder = false;
if (Content.IsEmpty())
{
ArgumentIndex = NextAutomaticIndex++;
bIsPlaceholder = true;
}
else if (TryParseIndex(Content, ArgumentIndex))
{
bIsPlaceholder = true;
}
else if (LooksLikeInvalidIndex(Content))
{
Result.bIsValid = false;
Result.Error = TEXT("invalid argument index '") + Content + TEXT("'");
return Result;
}
if (bIsPlaceholder)
{
if (ArgumentIndex < 0 || ArgumentIndex >= ArgumentCount)
{
Result.bIsValid = false;
Result.Error = TEXT("argument index ") + FString::FromInt(ArgumentIndex) + TEXT(" is out of range for ") + FString::FromInt(ArgumentCount) + TEXT(" argument(s)");
return Result;
}
Result.Literals.Add(MoveTemp(CurrentLiteral));
CurrentLiteral.Reset();
FFormatField Field;
Field.ArgumentIndex = ArgumentIndex;
Field.Specifier = MoveTemp(Specifier);
Result.Fields.Add(MoveTemp(Field));
}
else
{
// Named fields are unsupported, so preserve them literally.
CurrentLiteral.AppendChars(&InFormat[i], CloseIndex - i + 1);
}
i = CloseIndex;
continue;
}
if (Char == TEXT('}') && i + 1 < Len && InFormat[i + 1] == TEXT('}'))
{
CurrentLiteral.AppendChar(TEXT('}'));
++i;
continue;
}
CurrentLiteral.AppendChar(Char);
}
Result.Literals.Add(MoveTemp(CurrentLiteral));
return Result;
}
};
enum class ENumberPresentation : uint8
{
Decimal,
HexLower,
HexUpper,
BinaryLower,
BinaryUpper,
Float,
FloatUpper
};
struct FNumericFormatSpec
{
ENumberPresentation Presentation = ENumberPresentation::Decimal;
int32 Width = 0;
int32 Precision = 0;
bool bHasPrecision = false;
bool bAlternate = false;
bool bZeroPad = false;
bool bLeftAlign = false;
bool bAlwaysSign = false;
bool bSpaceSign = false;
FString MakePrintfBody() const
{
FString Result;
if (bLeftAlign) Result.AppendChar(TEXT('-'));
if (bAlwaysSign) Result.AppendChar(TEXT('+'));
else if (bSpaceSign) Result.AppendChar(TEXT(' '));
if (bAlternate) Result.AppendChar(TEXT('#'));
if (bZeroPad) Result.AppendChar(TEXT('0'));
if (Width > 0) Result.AppendInt(Width);
if (bHasPrecision)
{
Result.AppendChar(TEXT('.'));
Result.AppendInt(Precision);
}
return Result;
}
};
struct SafeFormatter
{
static constexpr int32 MaxWidthOrPrecision = 4096;
static constexpr int32 MaxFormattedChars = 16384;
static int32 RequiredBufferSize(const FNumericFormatSpec& Spec, const int32 BaseCharacters)
{
const int32 PrecisionCharacters = Spec.bHasPrecision ? Spec.Precision + BaseCharacters : BaseCharacters;
return FMath::Min(MaxFormattedChars, FMath::Max(PrecisionCharacters, Spec.Width + BaseCharacters));
}
template <typename ValueType>
static FString RunFormat(const int32 BufferSize, const FString& Fmt, const ValueType Value)
{
TArray<ANSICHAR> Buffer;
Buffer.SetNumUninitialized(FMath::Clamp(BufferSize, 2, MaxFormattedChars));
Buffer[0] = '\0';
const FTCHARToUTF8 ConvertedFormat(*Fmt);
const int32 Written = std::snprintf(Buffer.GetData(), Buffer.Num(), ConvertedFormat.Get(), Value);
return Written >= 0 && Written < Buffer.Num() ? FString(UTF8_TO_TCHAR(Buffer.GetData())) : FString(TEXT("[FormattedValueTooLong]"));
}
static bool ParseBoundedNumber(const FString& Spec, int32& Index, int32& OutValue)
{
const int32 StartIndex = Index;
int32 Value = 0;
while (Index < Spec.Len() && TChar<TCHAR>::IsDigit(Spec[Index]))
{
const int32 Digit = Spec[Index] - TEXT('0');
if (Value > (MaxWidthOrPrecision - Digit) / 10) return false;
Value = Value * 10 + Digit;
++Index;
}
if (Index == StartIndex) return false;
OutValue = Value;
return true;
}
static bool ParseNumericSpec(const FString& Spec, const bool bFloatingPoint, FNumericFormatSpec& Out)
{
Out = FNumericFormatSpec();
int32 Index = 0;
while (Index < Spec.Len())
{
const TCHAR Char = Spec[Index];
if (Char == TEXT('#')) Out.bAlternate = true;
else if (Char == TEXT('0')) Out.bZeroPad = true;
else if (Char == TEXT('-')) Out.bLeftAlign = true;
else if (Char == TEXT('+')) Out.bAlwaysSign = true;
else if (Char == TEXT(' ')) Out.bSpaceSign = true;
else break;
++Index;
}
if (Index < Spec.Len() && TChar<TCHAR>::IsDigit(Spec[Index]))
{
if (!ParseBoundedNumber(Spec, Index, Out.Width)) return false;
}
if (Index < Spec.Len() && Spec[Index] == TEXT('.'))
{
++Index;
Out.bHasPrecision = true;
if (!ParseBoundedNumber(Spec, Index, Out.Precision)) return false;
}
TCHAR Type = TEXT('\0');
if (Index < Spec.Len())
{
if (Index + 1 != Spec.Len()) return false;
Type = Spec[Index];
++Index;
}
if (Index != Spec.Len()) return false;
if (bFloatingPoint)
{
if (Type != TEXT('\0') && Type != TEXT('f') && Type != TEXT('F')) return false;
Out.Presentation = Type == TEXT('F') ? ENumberPresentation::FloatUpper : ENumberPresentation::Float;
return true;
}
switch (Type)
{
case TEXT('\0'):
case TEXT('d'):
case TEXT('i'): Out.Presentation = ENumberPresentation::Decimal; return true;
case TEXT('x'): Out.Presentation = ENumberPresentation::HexLower; return true;
case TEXT('X'): Out.Presentation = ENumberPresentation::HexUpper; return true;
case TEXT('b'): Out.Presentation = ENumberPresentation::BinaryLower; return true;
case TEXT('B'): Out.Presentation = ENumberPresentation::BinaryUpper; return true;
default: return false;
}
}
static FString InvalidSpec(const FString& Spec)
{
return TEXT("[InvalidFormatSpec:") + Spec + TEXT("]");
}
static FString FormatBinary(const uint64 Value, const FNumericFormatSpec& Spec)
{
FString Digits;
uint64 Remaining = Value;
if (Remaining == 0)
{
Digits = TEXT("0");
}
else
{
while (Remaining > 0)
{
Digits.InsertAt(0, (Remaining & 1) ? TEXT('1') : TEXT('0'));
Remaining >>= 1;
}
}
while (Spec.bHasPrecision && Digits.Len() < Spec.Precision)
{
Digits.InsertAt(0, TEXT('0'));
}
FString Prefix;
if (Spec.bAlternate)
{
Prefix = Spec.Presentation == ENumberPresentation::BinaryUpper ? TEXT("0B") : TEXT("0b");
}
const int32 PaddingCount = FMath::Max(0, Spec.Width - Prefix.Len() - Digits.Len());
FString Padding;
Padding.Reserve(PaddingCount);
for (int32 i = 0; i < PaddingCount; ++i) Padding.AppendChar(TEXT(' '));
if (Spec.bLeftAlign) return Prefix + Digits + Padding;
if (Spec.bZeroPad && !Spec.bHasPrecision)
{
FString ZeroPadding;
ZeroPadding.Reserve(PaddingCount);
for (int32 i = 0; i < PaddingCount; ++i) ZeroPadding.AppendChar(TEXT('0'));
return Prefix + ZeroPadding + Digits;
}
return Padding + Prefix + Digits;
}
static FString FormatSigned(const int64 Value, const FString& Spec)
{
FNumericFormatSpec Parsed;
if (!ParseNumericSpec(Spec, false, Parsed)) return InvalidSpec(Spec);
if (Parsed.Presentation == ENumberPresentation::BinaryLower || Parsed.Presentation == ENumberPresentation::BinaryUpper)
{
return FormatBinary(static_cast<uint64>(Value), Parsed);
}
FString Fmt = TEXT("%") + Parsed.MakePrintfBody();
if (Parsed.Presentation == ENumberPresentation::HexLower)
{
Fmt += TEXT("llx");
return RunFormat(RequiredBufferSize(Parsed, 128), Fmt, static_cast<unsigned long long>(Value));
}
if (Parsed.Presentation == ENumberPresentation::HexUpper)
{
Fmt += TEXT("llX");
return RunFormat(RequiredBufferSize(Parsed, 128), Fmt, static_cast<unsigned long long>(Value));
}
Fmt += TEXT("lld");
return RunFormat(RequiredBufferSize(Parsed, 128), Fmt, static_cast<long long>(Value));
}
static FString FormatUnsigned(const uint64 Value, const FString& Spec)
{
FNumericFormatSpec Parsed;
if (!ParseNumericSpec(Spec, false, Parsed)) return InvalidSpec(Spec);
if (Parsed.Presentation == ENumberPresentation::BinaryLower || Parsed.Presentation == ENumberPresentation::BinaryUpper)
{
return FormatBinary(Value, Parsed);
}
FString Fmt = TEXT("%") + Parsed.MakePrintfBody();
if (Parsed.Presentation == ENumberPresentation::HexLower) Fmt += TEXT("llx");
else if (Parsed.Presentation == ENumberPresentation::HexUpper) Fmt += TEXT("llX");
else Fmt += TEXT("llu");
return RunFormat(RequiredBufferSize(Parsed, 128), Fmt, static_cast<unsigned long long>(Value));
}
static FString FormatFloat(const double Value, const FString& Spec)
{
FNumericFormatSpec Parsed;
if (!ParseNumericSpec(Spec, true, Parsed)) return InvalidSpec(Spec);
const FString Fmt = TEXT("%") + Parsed.MakePrintfBody() + (Parsed.Presentation == ENumberPresentation::FloatUpper ? TEXT("F") : TEXT("f"));
return RunFormat(RequiredBufferSize(Parsed, 512), Fmt, Value);
}
};
template <typename T>
using Decayed = typename std::decay<T>::type;
template <typename T, typename = void>
struct THasToString : std::false_type {};
template <typename T>
struct THasToString<T, std::void_t<decltype(std::declval<const Decayed<T>&>().ToString())>>
: std::is_convertible<decltype(std::declval<const Decayed<T>&>().ToString()), FString> {};
template <typename T, typename = void>
struct TIsContainer : std::false_type {};
template <typename T>
struct TIsContainer<T, std::void_t<decltype(std::begin(std::declval<const Decayed<T>&>())), decltype(std::end(std::declval<const Decayed<T>&>()))>> : std::true_type {};
template <typename T>
struct TIsStringValue : std::is_constructible<FString, Decayed<T>> {};
template <typename T>
struct TIsCharacterType : std::integral_constant<bool, std::is_same<typename std::remove_cv<T>::type, TCHAR>::value || std::is_same<typename std::remove_cv<T>::type, ANSICHAR>::value || std::is_same<typename std::remove_cv<T>::type, WIDECHAR>::value || std::is_same<typename std::remove_cv<T>::type, UCS2CHAR>::value || std::is_same<typename std::remove_cv<T>::type, UTF8CHAR>::value> {};
template <typename T>
struct TIsCharacterPointer : std::integral_constant<bool, std::is_pointer<Decayed<T>>::value && TIsCharacterType<typename std::remove_pointer<Decayed<T>>::type>::value> {};
template <typename T>
constexpr bool IsContainerArgument = TIsContainer<T>::value && !TIsStringValue<T>::value;
template <typename T>
FString ElementToString(const T& Value, const FString& Spec = FString());
template <typename T>
FString ContainerToString(const T& Container, const FString& Spec)
{
FString Result = TEXT("[");
int32 Count = 0;
constexpr int32 MaxElements = 15;
for (const auto& Element : Container)
{
if (Count > 0) Result += TEXT(", ");
if (Count >= MaxElements)
{
Result += TEXT("...");
break;
}
Result += ElementToString(Element, Spec);
++Count;
}
Result += TEXT("]");
return Result;
}
template <typename T>
FString ElementToString(const T& Value, const FString& Spec)
{
using RawType = Decayed<T>;
if constexpr (std::is_same<RawType, std::nullptr_t>::value)
{
return Spec.IsEmpty() ? TEXT("nullptr") : SafeFormatter::InvalidSpec(Spec);
}
else if constexpr (std::is_pointer<RawType>::value)
{
if (!Spec.IsEmpty())
{
return SafeFormatter::FormatUnsigned(static_cast<uint64>(reinterpret_cast<UPTRINT>(Value)), Spec);
}
if constexpr (TIsCharacterPointer<RawType>::value)
{
return Value ? FString(Value) : TEXT("(NullString)");
}
else if constexpr (std::is_convertible<RawType, const UObject*>::value)
{
// Avoid dereferencing UObjects from worker threads.
if (!IsInGameThread()) return TEXT("[AsyncUObject]");
if (!IsValid(Value)) return TEXT("None");
if constexpr (std::is_convertible<RawType, const AActor*>::value)
{
return Value->GetActorNameOrLabel();
}
return Value->GetName();
}
else
{
return SafeFormatter::FormatUnsigned(static_cast<uint64>(reinterpret_cast<UPTRINT>(Value)), TEXT("#x"));
}
}
else if constexpr (IsContainerArgument<RawType>)
{
return ContainerToString(Value, Spec);
}
else if constexpr (std::is_same<RawType, bool>::value)
{
if (!Spec.IsEmpty())
{
return SafeFormatter::FormatUnsigned(Value ? 1u : 0u, Spec);
}
return Value ? TEXT("true") : TEXT("false");
}
else if constexpr (std::is_floating_point<RawType>::value)
{
if (!Spec.IsEmpty()) return SafeFormatter::FormatFloat(static_cast<double>(Value), Spec);
return FString::SanitizeFloat(Value);
}
else if constexpr (std::is_integral<RawType>::value)
{
if (Spec.IsEmpty()) return LexToString(Value);
if constexpr (std::is_signed<RawType>::value)
{
return SafeFormatter::FormatSigned(static_cast<int64>(Value), Spec);
}
else
{
return SafeFormatter::FormatUnsigned(static_cast<uint64>(Value), Spec);
}
}
else if constexpr (std::is_enum<RawType>::value)
{
if constexpr (TIsUEnumClass<RawType>::Value)
{
if (Spec.IsEmpty())
{
if (const UEnum* Enum = StaticEnum<RawType>())
{
const FString EnumName = Enum->GetNameStringByValue(static_cast<int64>(Value));
if (!EnumName.IsEmpty()) return EnumName;
}
}
}
using UnderlyingType = typename std::underlying_type<RawType>::type;
if constexpr (std::is_signed<UnderlyingType>::value)
{
const int64 NumericValue = static_cast<int64>(static_cast<UnderlyingType>(Value));
return Spec.IsEmpty() ? LexToString(NumericValue) : SafeFormatter::FormatSigned(NumericValue, Spec);
}
else
{
const uint64 NumericValue = static_cast<uint64>(static_cast<UnderlyingType>(Value));
return Spec.IsEmpty() ? LexToString(NumericValue) : SafeFormatter::FormatUnsigned(NumericValue, Spec);
}
}
else if constexpr (THasToString<RawType>::value)
{
return Spec.IsEmpty() ? Value.ToString() : SafeFormatter::InvalidSpec(Spec);
}
else if constexpr (TIsStringValue<RawType>::value)
{
return Spec.IsEmpty() ? FString(Value) : SafeFormatter::InvalidSpec(Spec);
}
else
{
return TEXT("[?]");
}
}
inline bool TryFormatArgumentAt(const int32, const FString&, FString&)
{
return false;
}
template <typename FirstType, typename... RestTypes>
bool TryFormatArgumentAt(const int32 Index, const FString& Spec, FString& Out, const FirstType& First, const RestTypes&... Rest)
{
if (Index == 0)
{
Out = ElementToString(First, Spec);
return true;
}
if (Index < 0) return false;
return TryFormatArgumentAt(Index - 1, Spec, Out, Rest...);
}
template <typename... ArgsTypes>
FString BuildUserMessage(const FString& Format, const ArgsTypes&... Arguments)
{
const FParsedFormat Parsed = FormatStringHelper::Parse(Format, sizeof...(Arguments));
if (!Parsed.bIsValid)
{
return TEXT("[EasyLog format error: ") + Parsed.Error + TEXT("] ") + Format;
}
FString Result = Parsed.Literals[0];
Result.Reserve(Format.Len() + 32);
for (int32 i = 0; i < Parsed.Fields.Num(); ++i)
{
FString FormattedArgument;
const FFormatField& Field = Parsed.Fields[i];
if (!TryFormatArgumentAt(Field.ArgumentIndex, Field.Specifier, FormattedArgument, Arguments...))
{
return TEXT("[EasyLog format error: argument lookup failed] ") + Format;
}
Result += FormattedArgument;
Result += Parsed.Literals[i + 1];
}
return Result;
}
#if EASY_LOG_MODERN
// ------------------------------------------------------------------------
// Modern path: UE5.2+ with C++20 source_location and UE_LOGFMT
// ------------------------------------------------------------------------
struct FSourceLoc
{
FString Function;
int32 Hash;
};
// The default argument is evaluated at the macro call site.
constexpr std::source_location Loc(const std::source_location& Location = std::source_location::current()) noexcept
{
return Location;
}
inline FSourceLoc ProcessLocation(const std::source_location& Location)
{
const char* FnAnsi = Location.function_name();
const int32 Line = static_cast<int32>(Location.line());
uint32 Hash = FCrc::MemCrc32(FnAnsi, std::strlen(FnAnsi));
Hash = (Hash ^ (static_cast<uint32>(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.FindChar(TEXT('('), Index)) FnName = FnName.Left(Index);
if (FnName.FindLastChar(TEXT(' '), Index)) FnName = FnName.RightChop(Index + 1);
return { FString::Printf(TEXT("%s:%d"), *FnName, Line), static_cast<int32>(Hash) };
}
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 FSourceLoc Location = ProcessLocation(SourceLocation);
const FString UserMessage = BuildUserMessage(Format, Arguments...);
LogToConsole<Verbosity>(FString::Printf(TEXT("%s | %s"), *UserMessage, *Location.Function));
if (GEngine && Duration > 0.f && IsInGameThread())
{
const FColor Color = Verbosity == ELogVerbosity::Error ? FColor::Red : Verbosity == ELogVerbosity::Warning ? FColor::Orange : FColor::White;
GEngine->AddOnScreenDebugMessage(Location.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 FSourceLoc Location = ProcessLocation(SourceLocation);
const FString UserMessage = BuildUserMessage(Format, Arguments...);
LogToConsole<Verbosity>(FString::Printf(TEXT("%s | %s"), *UserMessage, *Location.Function));
if (GEngine && Duration > 0.f && IsInGameThread())
{
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+ C++17 with UE_LOG
// ------------------------------------------------------------------------
#define EASY_UE_LOG_EXPAND(Category, Verbosity, Format, ...) UE_LOG(Category, Verbosity, Format, ##__VA_ARGS__)
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);
}
template <ELogVerbosity::Type Verbosity, typename... ArgsType>
void Dispatch(const int32 Key, const float Duration, const FString& Location, const FString& Format, ArgsType&&... Arguments)
{
const FString UserMessage = BuildUserMessage(Format, Arguments...);
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 && IsInGameThread())
{
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