diff --git a/Content/Widgets/WB_Console.uasset b/Content/Widgets/WB_Console.uasset index 52746273..7607bf09 100644 --- a/Content/Widgets/WB_Console.uasset +++ b/Content/Widgets/WB_Console.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76e9c59b6e9a30ec9a4572e57bf336350f14ad8fde444ba9e285fa063628a7b9 -size 239430 +oid sha256:265175100fea654209fe9b324d09a92fc3854b4fed5a60a14dc6e2fad847771e +size 263717 diff --git a/Source/Integration/FormatDataLibrary.cpp b/Source/Integration/FormatDataLibrary.cpp index 9e1a33fc..9eeb31fe 100644 --- a/Source/Integration/FormatDataLibrary.cpp +++ b/Source/Integration/FormatDataLibrary.cpp @@ -317,6 +317,12 @@ FFormatArgumentData UlxFormatDataLibrary::FormatArgumentDataEnum(uint8 Value, co return Result; } +FText UlxFormatDataLibrary::FormatMessageInternal(const FString &InPattern, TArray InArgs) +{ + FText InPatternText(FText::FromString(InPattern)); + return FTextFormatter::Format(MoveTemp(InPatternText), MoveTemp(InArgs), false, false); +} + void UlxFormatDataLibrary::FormatLogMessageInternal(UObject *Context, ElxFormatLogVerbosity Verbosity, const FString &InPattern, TArray InArgs) { // For throttled verbosity levels, suppress repeated messages with the @@ -338,8 +344,7 @@ void UlxFormatDataLibrary::FormatLogMessageInternal(UObject *Context, ElxFormatL // Generate the formatted string. // - FText InPatternText(FText::FromString(InPattern)); - FText Message = FTextFormatter::Format(MoveTemp(InPatternText), MoveTemp(InArgs), false, false); + FText Message = FormatMessageInternal(InPattern, MoveTemp(InArgs)); FString MessageString = Message.ToString(); // Get the blueprint name. diff --git a/Source/Integration/FormatDataLibrary.h b/Source/Integration/FormatDataLibrary.h index 7bf81a4a..f402afa2 100644 --- a/Source/Integration/FormatDataLibrary.h +++ b/Source/Integration/FormatDataLibrary.h @@ -105,9 +105,16 @@ public: // static UFunction* GetConverterForPinType(const UEdGraphSchema_K2 *Schema, const FEdGraphPinType& PinType, bool AllowWild); + // Format a message using FTextFormatter::Format. + // Meant to be used internally by the Format Message K2Node, + // which needs an impure wrapper around formatting. + // + UFUNCTION(BlueprintCallable, meta=(BlueprintInternalUseOnly = "true")) + static FText FormatMessageInternal(const FString &InPattern, TArray InArgs); + // Format a message using FTextFormatter::Format, and send // it to UE_LOG. The Context object's name is used as the - // log category. Meant to be used internally by the Format + // log category. Meant to be used internally by the Format // Log Message K2Node. // UFUNCTION(BlueprintCallable, meta=(WorldContext = "Context", BlueprintInternalUseOnly = "true")) diff --git a/Source/Integration/FormatMessage.cpp b/Source/Integration/FormatMessage.cpp index a672bfc3..85b4af1a 100644 --- a/Source/Integration/FormatMessage.cpp +++ b/Source/Integration/FormatMessage.cpp @@ -250,7 +250,7 @@ void UK2Node_FormatMessage::ExpandNode(class FKismetCompilerContext& CompilerCon } else { - FormatFunction = UKismetTextLibrary::StaticClass()->FindFunctionByName(GET_MEMBER_NAME_CHECKED(UKismetTextLibrary, Format)); + FormatFunction = UlxFormatDataLibrary::StaticClass()->FindFunctionByName(GET_MEMBER_NAME_CHECKED(UlxFormatDataLibrary, FormatMessageInternal)); } // This is the node that does all the Format work and outputs the message. diff --git a/Source/Integration/ReadSlashCommand.cpp b/Source/Integration/ReadSlashCommand.cpp new file mode 100644 index 00000000..7eb5b695 --- /dev/null +++ b/Source/Integration/ReadSlashCommand.cpp @@ -0,0 +1,226 @@ + + +#include "ReadSlashCommand.h" + +#include "BlueprintActionDatabaseRegistrar.h" +#include "BlueprintNodeSpawner.h" +#include "EdGraphSchema_K2.h" +#include "K2Node_CallFunction.h" +#include "KismetCompiler.h" +#include "LuaCall.h" +#include "SlashCommand.h" + +#define LOCTEXT_NAMESPACE "ReadSlashCommand" + +const FName UK2Node_ReadSlashCommand::PrototypePinName(TEXT("Prototype")); +const FName UK2Node_ReadSlashCommand::InputValuesPinName(TEXT("Input Values")); +const FName UK2Node_ReadSlashCommand::ErrorPinName(TEXT("Error")); + +bool UK2Node_ReadSlashCommand::ParsePrototype(const FString &Prototype, TArray& Steps) +{ + TArray Words; + Prototype.ParseIntoArrayWS(Words); + + // The command name must be a slash followed by alphanumerics. + if (Words.Num() == 0 || !UlxSlashCommand::IsSlashCommand(Words[0])) + { + SetErrorMsg(TEXT("The prototype must start with a command name, e.g. \"/foo\".")); + Steps.Empty(); + return false; + } + + Steps.SetNum(Words.Num()); + Steps[0].Word = Words[0]; + TSet UsedNames; + + for (int32 i = 1; i < Words.Num(); i++) + { + const FString& Word = Words[i]; + FString FuncName = FString(TEXT("Read")) + Word; + UFunction* ReadFunc = UlxSlashCommand::StaticClass()->FindFunctionByName(FName(*FuncName)); + if (ReadFunc == nullptr) + { + SetErrorMsg(FString::Printf(TEXT("Unknown value type: %s"), *Word)); + Steps.Empty(); + return false; + } + + FName PinName = AddPrefix(Word, 'R'); + for (int Suffix = 2; UsedNames.Contains(PinName); Suffix++) + { + PinName = AddPrefix(FString::Printf(TEXT("%s%d"), *Word, Suffix), 'R'); + } + Steps[i].Word = Word; + Steps[i].PinName = PinName; + Steps[i].ReadFunction = ReadFunc; + UsedNames.Add(PinName); + } + + return true; +} + +FText UK2Node_ReadSlashCommand::GetTooltipText() const +{ + static FText Tooltip = FText::FromString(TEXT( + "Parse a slash command.\n" + "\n" + "The prototype must be a hardwired string. The first word\n" + "is the command name; each remaining word names a value to\n" + "read and becomes an output pin.\n" + "\n" + "For example:\n" + "\n" + " /command Integer Float\n" + "\n" + "Supported types: Token, Integer, Float, Rest\n")); + return Tooltip; +} + +void UK2Node_ReadSlashCommand::ReconstructNode() +{ + // Save the value of the Prototype Pin before it gets reconstructed. + UEdGraphPin* PrototypePin = FindPin(PrototypePinName); + if (PrototypePin != nullptr) + { + ValuePrototype = PrototypePin->DefaultValue; + } + + Super::ReconstructNode(); +} + +void UK2Node_ReadSlashCommand::AllocateDefaultPins() +{ + Pins.Reset(); + Super::AllocateDefaultPins(); + + TArray Steps; + ParsePrototype(ValuePrototype, Steps); + + CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Execute); + CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Then); + CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, ErrorPinName); + + UEdGraphPin *PrototypePin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_String, PrototypePinName); + PrototypePin->DefaultValue = ValuePrototype; + + CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Object, UlxSlashCommand::StaticClass(), InputValuesPinName); + + // Create an output pin for each value word. + for (int32 i = 1; i < Steps.Num(); i++) + { + // The value comes back through the function's "Result" out-parameter. + CreatePin(EGPD_Output, PropertyToPinType(Steps[i].ReadFunction->FindPropertyByName(TEXT("Result"))), Steps[i].PinName); + } +} + + +FText UK2Node_ReadSlashCommand::GetNodeTitle(ENodeTitleType::Type TitleType) const +{ + return LOCTEXT("ReadSlashCommand_Title", "Read Slash Command"); +} + +FText UK2Node_ReadSlashCommand::GetPinDisplayName(const UEdGraphPin* Pin) const +{ + // These pins don't need labels. + if ((Pin->PinName == UEdGraphSchema_K2::PN_Execute) || + (Pin->PinName == UEdGraphSchema_K2::PN_Then) || + (Pin->PinName == PrototypePinName)) + { + return FText::GetEmpty(); + } + + // Return the pin name, removing R: prefix if present. + return FText::FromName(RemovePrefix(Pin->PinName)); +} + +void UK2Node_ReadSlashCommand::PinDefaultValueChanged(UEdGraphPin* Pin) +{ + if ((Pin->PinName == PrototypePinName) && (Pin->DefaultValue != ValuePrototype)) + { + ReconstructNode(); + } +} + + +void UK2Node_ReadSlashCommand::ExpandNode(class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) +{ + Super::ExpandNode(CompilerContext, SourceGraph); + + TArray Steps; + if (!ParsePrototype(ValuePrototype, Steps)) + { + CompilerContext.MessageLog.Error(*ErrorMsg); + BreakAllNodeLinks(); + return; + } + + UEdGraphPin *InputSlashCommandPin = FindPinChecked(InputValuesPinName); + UEdGraphPin *ErrorExecPin = FindPinChecked(ErrorPinName); + + UFunction *CheckCommandFunc = UlxSlashCommand::StaticClass()->FindFunctionByName(TEXT("CheckCommand")); + UK2Node_CallFunction *CheckCommandNode = MakeCallFunctionNode(CompilerContext, SourceGraph, CheckCommandFunc); + CompilerContext.CopyPinLinksToIntermediate(*InputSlashCommandPin, *CheckCommandNode->FindPinChecked(UEdGraphSchema_K2::PN_Self)); + CompilerContext.MovePinLinksToIntermediate(*GetExecPin(), *CheckCommandNode->GetExecPin()); + CheckCommandNode->FindPinChecked(TEXT("Literal"))->DefaultValue = Steps[0].Word; + CheckCommandNode->FindPinChecked(TEXT("Prototype"))->DefaultValue = ValuePrototype; + CompilerContext.CopyPinLinksToIntermediate(*ErrorExecPin, *CheckCommandNode->FindPinChecked(TEXT("Error"))); + UEdGraphPin *ThenPin = CheckCommandNode->FindPinChecked(TEXT("Success")); + + for (int32 i = 1; i < Steps.Num(); i++) + { + UK2Node_CallFunction *ReadNode = MakeCallFunctionNode(CompilerContext, SourceGraph, Steps[i].ReadFunction); + CompilerContext.CopyPinLinksToIntermediate(*InputSlashCommandPin, *ReadNode->FindPinChecked(UEdGraphSchema_K2::PN_Self)); + CompilerContext.CopyPinLinksToIntermediate(*ErrorExecPin, *ReadNode->FindPinChecked(TEXT("Error"))); + UEdGraphPin *OutputPin = FindPinChecked(Steps[i].PinName); + CompilerContext.MovePinLinksToIntermediate(*OutputPin, *ReadNode->FindPinChecked(TEXT("Result"))); + ThenPin = ChainExecPin(ThenPin, ReadNode, TEXT("Success")); + } + + CompilerContext.MovePinLinksToIntermediate(*GetThenPin(), *ThenPin); + + BreakAllNodeLinks(); +} + + +UK2Node::ERedirectType UK2Node_ReadSlashCommand::DoPinsMatchForReconstruction(const UEdGraphPin* NewPin, int32 NewPinIndex, const UEdGraphPin* OldPin, int32 OldPinIndex) const +{ + if (IsTemplate() || (GetGraph() == nullptr)) return ERedirectType_None; + if ((NewPin->PinName == OldPin->PinName) && + (NewPin->Direction == OldPin->Direction) && + (NewPin->PinType == OldPin->PinType)) + { + return ERedirectType_Name; + } + return ERedirectType_None; +} + +bool UK2Node_ReadSlashCommand::IsConnectionDisallowed(const UEdGraphPin* MyPin, const UEdGraphPin* OtherPin, FString& OutReason) const +{ + // The prototype pin cannot be connected. + if (MyPin->PinName == PrototypePinName) + { + OutReason = LOCTEXT("Error_PrototypeMustBeHardwired", "Value prototype must be a hardwired constant.").ToString(); + return true; + } + + return Super::IsConnectionDisallowed(MyPin, OtherPin, OutReason); +} + +void UK2Node_ReadSlashCommand::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const +{ + UClass* ActionKey = GetClass(); + if (ActionRegistrar.IsOpenForRegistration(ActionKey)) + { + UBlueprintNodeSpawner* NodeSpawner = UBlueprintNodeSpawner::Create(GetClass()); + check(NodeSpawner != nullptr); + ActionRegistrar.AddBlueprintAction(ActionKey, NodeSpawner); + } +} + +FText UK2Node_ReadSlashCommand::GetMenuCategory() const +{ + return FText::FromString(FString(TEXT("Luprex|Slash Commands"))); +} + + +#undef LOCTEXT_NAMESPACE diff --git a/Source/Integration/ReadSlashCommand.h b/Source/Integration/ReadSlashCommand.h new file mode 100644 index 00000000..0bbfa7e6 --- /dev/null +++ b/Source/Integration/ReadSlashCommand.h @@ -0,0 +1,74 @@ +//////////////////////////////////////////////////////////// +// +// ReadSlashCommand.h +// +// K2Node that reads typed values from a UlxLuaValues array. +// Takes a prototype string like "string x, float y, int z" +// and creates output pins with the appropriate types. +// +//////////////////////////////////////////////////////////// + +#pragma once + +#include "LuprexK2Node.h" + +#include "ReadSlashCommand.generated.h" + +class FBlueprintActionDatabaseRegistrar; +class FString; +class UEdGraph; +class UObject; + +UCLASS(MinimalAPI) +class UK2Node_ReadSlashCommand : public UlxK2Node +{ + GENERATED_BODY() + +public: + //~ Begin UEdGraphNode Interface. + virtual void AllocateDefaultPins() override; + virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override; + virtual bool ShouldShowNodeProperties() const override { return true; } + virtual void PinDefaultValueChanged(UEdGraphPin* Pin) override; + virtual FText GetTooltipText() const override; + virtual FText GetPinDisplayName(const UEdGraphPin* Pin) const override; + //~ End UEdGraphNode Interface. + + //~ Begin UK2Node Interface. + virtual bool IsNodePure() const override { return false; } + virtual void ReconstructNode() override; + virtual bool NodeCausesStructuralBlueprintChange() const override { return true; } + virtual void ExpandNode(class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) override; + virtual ERedirectType DoPinsMatchForReconstruction(const UEdGraphPin* NewPin, int32 NewPinIndex, const UEdGraphPin* OldPin, int32 OldPinIndex) const override; + virtual bool IsConnectionDisallowed(const UEdGraphPin* MyPin, const UEdGraphPin* OtherPin, FString& OutReason) const override; + virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override; + virtual FText GetMenuCategory() const override; + virtual int32 GetNodeRefreshPriority() const override { return EBaseNodeRefreshPriority::Low_UsesDependentWildcard; } + //~ End UK2Node Interface. + +private: + static const FName PrototypePinName; + static const FName InputValuesPinName; + static const FName ErrorPinName; + + struct ParsingStep + { + FString Word; + FName PinName; + UFunction *ReadFunction; + }; + + bool ParsePrototype(const FString &Prototype, TArray& Steps); + +private: + // Whenever the prototype pin value changes, we call + // ReconstructNode, which backs up the value into this + // property. This cache is needed because during + // ReconstructNode, we blow away the prototype pin. The + // prototype pin is also absent when the node is first + // created. + // + UPROPERTY() + FString ValuePrototype = TEXT("/command Integer Float"); + +}; diff --git a/Source/Integration/SlashCommand.cpp b/Source/Integration/SlashCommand.cpp new file mode 100644 index 00000000..9a9a8b69 --- /dev/null +++ b/Source/Integration/SlashCommand.cpp @@ -0,0 +1,157 @@ +#include "SlashCommand.h" +#include "CoreMinimal.h" +#include "Containers/StringView.h" +#include "Misc/Char.h" +#include "Misc/DefaultValueHelper.h" + +////////////////////////////////////////////////////////////// +// +// SlashCommand +// +// A command line plus a cursor, exposed to blueprint. +// +////////////////////////////////////////////////////////////// + + +UlxSlashCommand* UlxSlashCommand::MakeSlashCommand(const FString& CommandLine) +{ + UlxSlashCommand* Result = NewObject(); + Result->CommandLine = CommandLine; + return Result; +} + + +bool UlxSlashCommand::IsSlashCommand(const FString& Command) +{ + if (Command.Len() < 2 || Command[0] != TEXT('/')) + { + return false; + } + + // Every character after the slash must be alphanumeric. + for (int32 i = 1; i < Command.Len(); i++) + { + if (!FChar::IsAlnum(Command[i])) + { + return false; + } + } + + return true; +} + + +FStringView UlxSlashCommand::FetchToken() +{ + const TCHAR* Data = *CommandLine; + int32 Len = CommandLine.Len(); + + // Skip leading whitespace. + while (Cursor < Len && FChar::IsWhitespace(Data[Cursor])) + { + Cursor++; + } + + // Read characters up to the next whitespace. + int32 Start = Cursor; + while (Cursor < Len && !FChar::IsWhitespace(Data[Cursor])) + { + Cursor++; + } + + return FStringView(Data + Start, Cursor - Start); +} + +ElxSuccessOrError UlxSlashCommand::CheckCommand(const FString& Literal, const FString& Prototype) +{ + KnownCommands.Add(Literal); + + // Checking the command always starts a fresh parse. + Cursor = 0; + FStringView Token = FetchToken(); + if (Token.Equals(Literal, ESearchCase::IgnoreCase)) + { + MatchingPrototypes.Add(Prototype); + return ElxSuccessOrError::Success; + } + + return ElxSuccessOrError::Error; +} + +ElxSuccessOrError UlxSlashCommand::ReadToken(FString& Result) +{ + FStringView Token = FetchToken(); + if (Token.IsEmpty()) + { + Result.Empty(); + return ElxSuccessOrError::Error; + } + + Result = FString(Token); + return ElxSuccessOrError::Success; +} + + +ElxSuccessOrError UlxSlashCommand::ReadInteger(int32& Result) +{ + // ParseInt validates the whole token and converts it, so "12abc" + // is rejected rather than read as a partial number. + FStringView Token = FetchToken(); + if (!FDefaultValueHelper::ParseInt(FString(Token), Result)) + { + Result = 0; + return ElxSuccessOrError::Error; + } + + return ElxSuccessOrError::Success; +} + + +ElxSuccessOrError UlxSlashCommand::ReadFloat(double& Result) +{ + // ParseDouble validates the whole token and converts it, so + // "12abc" is rejected rather than read as a partial number. + FStringView Token = FetchToken(); + if (!FDefaultValueHelper::ParseDouble(FString(Token), Result)) + { + Result = 0.0; + return ElxSuccessOrError::Error; + } + + return ElxSuccessOrError::Success; +} + + +ElxSuccessOrError UlxSlashCommand::ReadRest(FString& Result) +{ + const TCHAR* Data = *CommandLine; + int32 Len = CommandLine.Len(); + + // Everything from the cursor to the end of the line, trimmed. + Result = FString(FStringView(Data + Cursor, Len - Cursor)); + Result.TrimStartAndEndInline(); + Cursor = Len; + return ElxSuccessOrError::Success; +} + +FString UlxSlashCommand::GetErrorMessage() const +{ + if (MatchingPrototypes.Num() == 0) + { + TArray Commands; + for (const FString& Command : KnownCommands) + { + Commands.Add(Command); + } + Commands.Sort(); + return FString(TEXT("No such slash command. Valid slash commands: ")) + FString::Join(Commands, TEXT(", ")); + } + + FString Result = TEXT("Invalid parameters. Valid parameters:\n"); + for (const FString& Prototype : MatchingPrototypes) + { + Result += Prototype; + Result += TEXT("\n"); + } + return Result; +} diff --git a/Source/Integration/SlashCommand.h b/Source/Integration/SlashCommand.h new file mode 100644 index 00000000..71225c54 --- /dev/null +++ b/Source/Integration/SlashCommand.h @@ -0,0 +1,121 @@ +//////////////////////////////////////////////////////////// +// +// SlashCommand.h +// +// A command line plus a cursor, exposed to blueprint. +// +//////////////////////////////////////////////////////////// + +#pragma once + +#include "Containers/Array.h" +#include "Containers/Set.h" +#include "Containers/UnrealString.h" +#include "Containers/StringFwd.h" +#include "Common.h" +#include "SlashCommand.generated.h" + +//////////////////////////////////////////////////////////// +// +// UlxSlashCommand +// +// Holds a command line (a string the user has typed) together +// with a cursor marking the current position within it. This +// is the object that wraps Unreal's FParse facilities so that +// blueprint can parse a typed command piece by piece, with the +// cursor advancing as each token is consumed. +// +//////////////////////////////////////////////////////////// + +UCLASS(BlueprintType) +class UlxSlashCommand : public UObject +{ + GENERATED_BODY() + +private: + // The full command line that we are parsing. + // + FString CommandLine; + + // The current parse position: an index into CommandLine. + // + int32 Cursor = 0; + + // Known command names that have been registered while exploring + // possible parses for this line. + // + TSet KnownCommands; + + // Prototypes whose command name matches this line. + // + TArray MatchingPrototypes; + + // Skip leading whitespace at the cursor, then read characters up + // to (but not including) the next whitespace. The token is + // returned as a view into CommandLine, and the cursor is advanced + // past it. + // + // Returns an empty view if there is no nonwhitespace input left. + // + FStringView FetchToken(); + +public: + // Construct a slash command from a command line string. + // The cursor starts at the beginning. + // + UFUNCTION(BlueprintCallable) + static UlxSlashCommand* MakeSlashCommand(const FString& CommandLine); + + // Return true if the string is a slash followed by one or more + // alphanumeric characters, and nothing else (e.g. "/foo"). + // + static bool IsSlashCommand(const FString& Command); + + // Reset the cursor to the start, then read the first token and + // check whether it matches the given literal, case-insensitively. + // The command name is recorded in KnownCommands either way. If it + // matched, the prototype is added to MatchingPrototypes. The token + // is consumed either way. Returns Success if it matched, Error + // otherwise. + // + UFUNCTION(BlueprintCallable, meta = (ExpandEnumAsExecs = "ReturnValue")) + ElxSuccessOrError CheckCommand(const FString& Literal, const FString& Prototype); + + // Read the next whitespace-delimited word from the command line. + // + // This is the blueprint-callable form of FetchToken: it returns + // the token as an FString. Returns Error (with an empty Result) + // if there is no nonwhitespace input left, Success otherwise. + // + UFUNCTION(BlueprintCallable, meta = (ExpandEnumAsExecs = "ReturnValue")) + ElxSuccessOrError ReadToken(FString& Result); + + // Read the next token and interpret it as an integer, using C++ + // number syntax (optional sign; decimal, 0x hex, or leading-0 + // octal). The whole token must be valid; "12abc" is rejected. + // Returns Error (with Result 0) on failure, Success otherwise. + // + UFUNCTION(BlueprintCallable, meta = (ExpandEnumAsExecs = "ReturnValue")) + ElxSuccessOrError ReadInteger(int32& Result); + + // Read the next token and interpret it as a floating-point number, + // using C++ number syntax (optional sign, digits, dot, e/E + // exponent, trailing f). The whole token must be valid; "12abc" + // is rejected. Returns Error (with Result 0) on failure, Success + // otherwise. + // + UFUNCTION(BlueprintCallable, meta = (ExpandEnumAsExecs = "ReturnValue")) + ElxSuccessOrError ReadFloat(double& Result); + + // Read the rest of the command line, from the cursor to the end, + // trimmed of leading and trailing whitespace. The cursor is + // advanced to the end. Always returns Success. + // + UFUNCTION(BlueprintCallable, meta = (ExpandEnumAsExecs = "ReturnValue")) + ElxSuccessOrError ReadRest(FString& Result); + + // Return a user-facing error message describing why parsing failed. + // + UFUNCTION(BlueprintCallable) + FString GetErrorMessage() const; +};