Lots of refactors related to BreakToDebugger and FormatLogMessage

This commit is contained in:
2026-02-14 01:25:04 -05:00
parent 96256d7836
commit dd159b064d
10 changed files with 221 additions and 162 deletions

View File

@@ -1,117 +0,0 @@
//
// BlueprintErrors: Better error handling for blueprint errors.
//
#pragma once
#include "Containers/Array.h"
#include "CoreMinimal.h"
#include "HAL/Platform.h"
#include "Misc/OutputDeviceError.h"
#include "UObject/NameTypes.h"
#include "UObject/ObjectMacros.h"
#include "UObject/UObjectGlobals.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "BlueprintErrors.generated.h"
/*
* enum class ElxLogVerbosity, below, contains all the same error severity levels
* as ELogVerbosity, but in a form that the blueprint editor can manipulate.
*
* We deliberately moved 'Fatal' to the end of the list, and made 'Error' option 0.
* We did that because we want the editor to default to 'Error' in most cases.
* Unfortunately, that means the numeric values of the two enums don't match up,
* so we will need a conversion function.
*
* ThrottledDisplay and ThrottledLog are not present in ELogVerbosity. They
* behave like Display and Log respectively, but suppress repeated messages
* with the same format pattern, logging at most once per second.
*
*/
/** Log Verbosity: The importance of an error message, which affects which logs the error
* message gets written to, and how that message gets filtered.
*
*/
UENUM(BlueprintType)
enum class ElxLogVerbosity : uint8 {
/* Prints an error to the console and log file. The editor collects and reports errors. */
Error,
/* Prints a warning to the console and log file. The editor collects and report warnings. */
Warning,
/* Prints a message to the console and log file. */
Display,
/* Prints a message to the log file, however, it does not print to the console. */
Log,
/* Like Display, but suppresses repeated messages with the same format pattern (at most once per second). */
ThrottledDisplay,
/* Like Log, but suppresses repeated messages with the same format pattern (at most once per second). */
ThrottledLog,
/* Prints a message to a log file only if Verbose logging is enabled for the given category. This is usually used for detailed logging. */
Verbose,
/* Prints a message to a log file. If VeryVerbose logging is enabled, then this is used for detailed logging that would otherwise spam output. */
VeryVerbose,
/* Danger! Prints a fatal error to the console and log file, then crashes (this crashes the editor too). */
Fatal,
};
/* A library containing assorted useful functions for blueprint error handling.
*
*/
UCLASS(MinimalAPI)
class UlxBlueprintErrorLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
// Convert an ElxLogVerbosity to an ELogVerbosity::Type
//
static ELogVerbosity::Type ConvertElxLogVerbosity(ElxLogVerbosity Verbosity);
};
/* Debug Blueprint Errors output device.
*
* When an error message gets written to the log, using "Format Error Message,"
* or any other means that writes an error message to the log,
* we can optionally notify the blueprint debugger to pause execution.
* This only affects errors that are generated during blueprint execution.
* Errors in other threads do not pause the blueprint.
*
*/
struct FlxDebugBlueprintErrorsOutputDevice : public FOutputDevice
{
public:
// The constructor and destructor automatically register this output device with GLog.
//
// This struct doesn't store the sensitivity threshold. It relies on some blueprint
// class to do that, so that the threshold can be easily edited with the blueprint
// editor. This struct must be initialized with a reference to the threshold variable.
//
FlxDebugBlueprintErrorsOutputDevice(const ElxLogVerbosity &SensitivityRef);
~FlxDebugBlueprintErrorsOutputDevice();
// Inspect a log message.
//
INTEGRATION_API virtual void Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity, const FName& Category) override;
// If the device is marked 'CanBeUsedOnMultipleThreads,' then UE_LOG will
// call Serialize from the current thread, otherwise, it will call Serialize from
// the logging thread. Using the logging thread would defeat the purpose of this
// device, so it's imperative that we set this flag.
//
INTEGRATION_API virtual bool CanBeUsedOnMultipleThreads() const override { return true; }
private:
const ElxLogVerbosity &Sensitivity;
};

View File

@@ -1,29 +1,27 @@
#include "BlueprintErrors.h"
#include "BreakToDebugger.h"
#include "Blueprint/BlueprintExceptionInfo.h"
#include "Kismet2/KismetDebugUtilities.h"
ELogVerbosity::Type UlxBlueprintErrorLibrary::ConvertElxLogVerbosity(ElxLogVerbosity Verbosity) {
ELogVerbosity::Type FlxBreakToDebuggerOutputDevice::ConvertThreshold(ElxBreakToDebuggerThreshold Verbosity) {
switch (Verbosity) {
case ElxLogVerbosity::Error: return ELogVerbosity::Error;
case ElxLogVerbosity::Warning: return ELogVerbosity::Warning;
case ElxLogVerbosity::Display: return ELogVerbosity::Display;
case ElxLogVerbosity::Log: return ELogVerbosity::Log;
case ElxLogVerbosity::ThrottledDisplay: return ELogVerbosity::Display;
case ElxLogVerbosity::ThrottledLog: return ELogVerbosity::Log;
case ElxLogVerbosity::Verbose: return ELogVerbosity::Verbose;
case ElxLogVerbosity::VeryVerbose: return ELogVerbosity::VeryVerbose;
case ElxLogVerbosity::Fatal: return ELogVerbosity::Fatal;
case ElxBreakToDebuggerThreshold::Error: return ELogVerbosity::Error;
case ElxBreakToDebuggerThreshold::Warning: return ELogVerbosity::Warning;
case ElxBreakToDebuggerThreshold::Display: return ELogVerbosity::Display;
case ElxBreakToDebuggerThreshold::Log: return ELogVerbosity::Log;
case ElxBreakToDebuggerThreshold::Verbose: return ELogVerbosity::Verbose;
case ElxBreakToDebuggerThreshold::VeryVerbose: return ELogVerbosity::VeryVerbose;
case ElxBreakToDebuggerThreshold::Fatal: return ELogVerbosity::Fatal;
}
}
FlxDebugBlueprintErrorsOutputDevice::FlxDebugBlueprintErrorsOutputDevice(const ElxLogVerbosity &SensitivityRef)
FlxBreakToDebuggerOutputDevice::FlxBreakToDebuggerOutputDevice(const ElxBreakToDebuggerThreshold &SensitivityRef)
: Sensitivity(SensitivityRef)
{
GLog->AddOutputDevice(this);
}
FlxDebugBlueprintErrorsOutputDevice::~FlxDebugBlueprintErrorsOutputDevice()
FlxBreakToDebuggerOutputDevice::~FlxBreakToDebuggerOutputDevice()
{
GLog->RemoveOutputDevice(this);
}
@@ -46,11 +44,11 @@ namespace UBreakPoint {
static const FName LogBlueprintDebugName(TEXT("LogBlueprintDebug"));
void FlxDebugBlueprintErrorsOutputDevice::Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity, const FName& Category)
void FlxBreakToDebuggerOutputDevice::Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity, const FName& Category)
{
// If the error isn't serious enough, do nothing.
//
if (Verbosity > UlxBlueprintErrorLibrary::ConvertElxLogVerbosity(Sensitivity))
if (Verbosity > ConvertThreshold(Sensitivity))
{
return;
}

View File

@@ -0,0 +1,113 @@
////////////////////////////////////////////////////////////
//
// BreakToDebugger
//
// When an error message gets written to UE_LOG, we can
// optionally trigger the blueprint debugger.
//
// This only affects UE_LOG messages that are generated
// during blueprint execution. Log messages from other
// threads do not trigger the debugger.
//
// The following explains how we trigger the blueprint
// debugger on UE_LOG messages. Log messages are sent to a
// long list of output devices, including: the visual studio
// output window, the unreal editor output window, the log
// file, and so forth. We add another pseudo output device.
// This output device doesn't actually send the log message
// anywhere, instead, it just activates the blueprint
// debugger.
//
// UE_LOG messages can be generated from any thread. The
// pseudo output device checks what thread it is running in,
// and if it's not the blueprint thread, it does nothing at
// all. If it is the blueprint thread, it's safe to trigger
// the blueprint debugger.
//
// One annoying limitation of this design is that our output
// device ends up early in the list, so the debugger runs
// *before* the message shows up in visual studio or the
// unreal editor. As a result, when you are in the
// debugger, the message won't be in your output window.
// Pressing 'single step' always reveals the message.
//
// The blueprint node "Format Log Message" uses UE_LOG
// internally, so therefore, that too can trigger the
// blueprint debugger.
//
////////////////////////////////////////////////////////////
#pragma once
#include "Containers/Array.h"
#include "CoreMinimal.h"
#include "HAL/Platform.h"
#include "Misc/OutputDeviceError.h"
#include "UObject/NameTypes.h"
#include "UObject/ObjectMacros.h"
#include "UObject/UObjectGlobals.h"
#include "BreakToDebugger.generated.h"
// ElxBreakToDebuggerThreshold:
//
// Controls the sensitivity level at which UE_LOG messages
// trigger the blueprint debugger.
//
UENUM(BlueprintType)
enum class ElxBreakToDebuggerThreshold : uint8 {
/* Break on errors. */
Error,
/* Break on warnings and above. */
Warning,
/* Break on display messages and above. */
Display,
/* Break on log messages and above. */
Log,
/* Break on verbose messages and above. */
Verbose,
/* Break on all messages. */
VeryVerbose,
/* Break on fatal errors only (ie, never break -- the process crashes instead). */
Fatal,
};
struct FlxBreakToDebuggerOutputDevice : public FOutputDevice
{
public:
// The constructor and destructor automatically register
// this output device with GLog.
//
// This struct doesn't store the sensitivity threshold.
// It relies on the LuprexGameMode class to do that, so
// that the threshold can be easily edited with the
// blueprint editor. This struct must be initialized
// with a reference to the threshold variable.
//
FlxBreakToDebuggerOutputDevice(const ElxBreakToDebuggerThreshold &SensitivityRef);
~FlxBreakToDebuggerOutputDevice();
// Inspect a log message.
//
INTEGRATION_API virtual void Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity, const FName& Category) override;
// If the device is marked 'CanBeUsedOnMultipleThreads,'
// then UE_LOG will call Serialize from the current
// thread, otherwise, it will call Serialize from the
// logging thread. Using the logging thread would
// defeat the purpose of this device, so it's imperative
// that we set this flag.
//
INTEGRATION_API virtual bool CanBeUsedOnMultipleThreads() const override { return true; }
private:
static ELogVerbosity::Type ConvertThreshold(ElxBreakToDebuggerThreshold Verbosity);
const ElxBreakToDebuggerThreshold &Sensitivity;
};

View File

@@ -1,3 +1,26 @@
//////////////////////////////////////////////////////////////
//
// ConsoleOutput
//
// This class can optionally be used by the GameMode
// blueprint to help implement the console window. There is
// no requirement that the blueprint use this class, it can
// implement the console window any way it wants, this is
// just here in case it is desired.
//
// This class stores the text that's in the unreal console.
// It stores it as one great big string, which contains
// newlines to denote line breaks.
//
// This class also contains a 'dirty' bit. Each time
// somebody appends a line of text to the console, the dirty
// bit is automatically set. The bit can be checked using
// 'IsDirty' and cleared using 'ClearDirty'. This makes it
// so that you don't have to update the unreal widget unless
// the text has actually changed.
//
//////////////////////////////////////////////////////////////
#pragma once
#include "Containers/UnrealString.h"
@@ -5,23 +28,6 @@
#include "ConsoleOutput.generated.h"
//////////////////////////////////////////////////////////////
//
// ConsoleOutput
//
// This class stores the text that's in the unreal console.
// It stores it as one great big string, which contains
// newlines to denote line breaks.
//
// This class also contains a 'dirty' bit. Each time somebody
// appends a line of text to the console, the dirty bit is
// automatically set. The bit can be checked using 'IsDirty'
// and cleared using 'ClearDirty'. This makes it so that
// you don't have to update the unreal widget unless the
// text has actually changed.
//
//////////////////////////////////////////////////////////////
UCLASS(BlueprintType)
class UlxConsoleOutput : public UObject
{

View File

@@ -108,7 +108,7 @@ void UK2Node_FormatMessage::CreateCorrectPins()
if (IsFormatErrorMessage())
{
if (FindPin(VerbosityPinName, EGPD_Input) == nullptr) {
UEdGraphPin *P = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Byte, StaticEnum<ElxLogVerbosity>(), VerbosityPinName);
UEdGraphPin *P = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Byte, StaticEnum<ElxFormatLogVerbosity>(), VerbosityPinName);
P->DefaultValue = TEXT("Error");
P->AutogeneratedDefaultValue = P->DefaultValue;
}
@@ -573,13 +573,27 @@ UK2Node_FormatLogMessage::UK2Node_FormatLogMessage(const FObjectInitializer& Obj
);
}
void UK2Node_FormatMessage::FormatLogMessageInternal(UObject *Context, ElxLogVerbosity Verbosity, const FString &InPattern, TArray<FFormatArgumentData> InArgs)
ELogVerbosity::Type UK2Node_FormatMessage::ConvertElxFormatLogVerbosity(ElxFormatLogVerbosity Verbosity) {
switch (Verbosity) {
case ElxFormatLogVerbosity::Error: return ELogVerbosity::Error;
case ElxFormatLogVerbosity::Warning: return ELogVerbosity::Warning;
case ElxFormatLogVerbosity::Display: return ELogVerbosity::Display;
case ElxFormatLogVerbosity::Log: return ELogVerbosity::Log;
case ElxFormatLogVerbosity::ThrottledDisplay: return ELogVerbosity::Display;
case ElxFormatLogVerbosity::ThrottledLog: return ELogVerbosity::Log;
case ElxFormatLogVerbosity::Verbose: return ELogVerbosity::Verbose;
case ElxFormatLogVerbosity::VeryVerbose: return ELogVerbosity::VeryVerbose;
case ElxFormatLogVerbosity::Fatal: return ELogVerbosity::Fatal;
}
}
void UK2Node_FormatMessage::FormatLogMessageInternal(UObject *Context, ElxFormatLogVerbosity Verbosity, const FString &InPattern, TArray<FFormatArgumentData> InArgs)
{
// For throttled verbosity levels, suppress repeated messages with the
// same format pattern. We key on the blueprint name + format pattern,
// and allow at most one message per second per key.
//
if (Verbosity == ElxLogVerbosity::ThrottledDisplay || Verbosity == ElxLogVerbosity::ThrottledLog)
if (Verbosity == ElxFormatLogVerbosity::ThrottledDisplay || Verbosity == ElxFormatLogVerbosity::ThrottledLog)
{
static TMap<FString, double> LastLogTime;
double Now = FPlatformTime::Seconds();
@@ -618,7 +632,7 @@ void UK2Node_FormatMessage::FormatLogMessageInternal(UObject *Context, ElxLogVer
// Output to Log
//
ELogVerbosity::Type VerbosityValue = UlxBlueprintErrorLibrary::ConvertElxLogVerbosity(Verbosity);
ELogVerbosity::Type VerbosityValue = ConvertElxFormatLogVerbosity(Verbosity);
if (VerbosityValue <= ELogVerbosity::COMPILED_IN_MINIMUM_VERBOSITY)
{
FMsg::Logf(BlueprintNameAnsi.Get(), 0, BlueprintNameLogCategory, VerbosityValue, TEXT("%s"), *MessageString);

View File

@@ -2,7 +2,7 @@
#pragma once
#include "BlueprintErrors.h"
#include "BreakToDebugger.h"
#include "FormatDataLibrary.h"
#include "Containers/Array.h"
#include "CoreMinimal.h"
@@ -14,10 +14,51 @@
#include "UObject/NameTypes.h"
#include "UObject/ObjectMacros.h"
#include "UObject/UObjectGlobals.h"
#include "BlueprintErrors.h"
#include "FormatMessage.generated.h"
/** Format Log Verbosity: controls how a message from the "Format Log Message"
* K2Node gets written to the log.
*
* 'Fatal' is deliberately placed at the end so that the editor defaults to
* 'Error' (value 0) when the dropdown is uninitialized. This means the
* numeric values don't match ELogVerbosity, so a conversion function is needed.
*
* ThrottledDisplay and ThrottledLog behave like Display and Log respectively,
* but suppress repeated messages with the same format pattern, logging at most
* once per second.
*/
UENUM(BlueprintType)
enum class ElxFormatLogVerbosity : uint8 {
/* Prints an error to the console and log file. The editor collects and reports errors. */
Error,
/* Prints a warning to the console and log file. The editor collects and report warnings. */
Warning,
/* Prints a message to the console and log file. */
Display,
/* Prints a message to the log file, however, it does not print to the console. */
Log,
/* Like Display, but suppresses repeated messages with the same format pattern (at most once per second). */
ThrottledDisplay,
/* Like Log, but suppresses repeated messages with the same format pattern (at most once per second). */
ThrottledLog,
/* Prints a message to a log file only if Verbose logging is enabled for the given category. This is usually used for detailed logging. */
Verbose,
/* Prints a message to a log file. If VeryVerbose logging is enabled, then this is used for detailed logging that would otherwise spam output. */
VeryVerbose,
/* Danger! Prints a fatal error to the console and log file, then crashes (this crashes the editor too). */
Fatal,
};
class FBlueprintActionDatabaseRegistrar;
class FString;
class UEdGraph;
@@ -82,7 +123,10 @@ protected:
// function, which formats the message and outputs it to the log.
//
UFUNCTION(BlueprintCallable, meta=(WorldContext = "Context", BlueprintInternalUseOnly = "true"))
static void FormatLogMessageInternal(UObject *Context, ElxLogVerbosity Verbosity, const FString &InPattern, TArray<FFormatArgumentData> InArgs);
static void FormatLogMessageInternal(UObject *Context, ElxFormatLogVerbosity Verbosity, const FString &InPattern, TArray<FFormatArgumentData> InArgs);
private:
static ELogVerbosity::Type ConvertElxFormatLogVerbosity(ElxFormatLogVerbosity Verbosity);
protected:
/** When adding arguments to the node, their names are placed here and are generated as pins during construction */

View File

@@ -2,7 +2,6 @@
#pragma once
#include "BlueprintErrors.h"
#include "Containers/Array.h"
#include "CoreMinimal.h"
#include "EdGraph/EdGraphNode.h"
@@ -13,7 +12,6 @@
#include "UObject/NameTypes.h"
#include "UObject/ObjectMacros.h"
#include "UObject/UObjectGlobals.h"
#include "BlueprintErrors.h"
#include "LuaCallNode.generated.h"

View File

@@ -336,7 +336,7 @@ void ALuprexGameModeBase::InitializeGlobalState()
// If somebody generates a log message that's severe enough, break to debugger.
BreakToDebuggerLogVerbosityDevice.Reset(
new FlxDebugBlueprintErrorsOutputDevice(BreakToDebuggerLogVerbosity));
new FlxBreakToDebuggerOutputDevice(BreakToDebuggerLogVerbosity));
}
void ALuprexGameModeBase::EndPlay(const EEndPlayReason::Type EndPlayReason)

View File

@@ -10,7 +10,7 @@
#include "AssetLookup.h"
#include "LuprexSockets.h"
#include "TriggeredTask.h"
#include "BlueprintErrors.h"
#include "BreakToDebugger.h"
#include "Blueprint/UserWidget.h"
#include "Widgets/CommonActivatableWidgetContainer.h"
#include "CommonActivatableWidget.h"
@@ -160,7 +160,7 @@ public:
// The sensitivity level at which a log message triggers a debugger breakpoint.
UPROPERTY(EditAnywhere, Category="Debugging Tools")
ElxLogVerbosity BreakToDebuggerLogVerbosity;
ElxBreakToDebuggerThreshold BreakToDebuggerLogVerbosity;
// The Luprex EngineWrapper, with a Mutex to protect it.
// To access it, construct a FlxLockedWrapper.
@@ -195,5 +195,5 @@ public:
FDelegateHandle OnWorldPostActorTickHandle;
// The device that implements BreakToDebuggerLogVerbosity, above.
TUniquePtr<FlxDebugBlueprintErrorsOutputDevice> BreakToDebuggerLogVerbosityDevice;
TUniquePtr<FlxBreakToDebuggerOutputDevice> BreakToDebuggerLogVerbosityDevice;
};