Last active
February 16, 2026 14:27
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 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