664 lines
18 KiB
C++
664 lines
18 KiB
C++
#include "WingUtils.h"
|
|
#include "WingActorComponent.h"
|
|
#include "WingJson.h"
|
|
#include "WingTypes.h"
|
|
#include "WingServer.h"
|
|
#include "WingHandler.h"
|
|
#include "Engine/Blueprint.h"
|
|
#include "Engine/MemberReference.h"
|
|
#include "Engine/World.h"
|
|
#include "Components/ActorComponent.h"
|
|
#include "Engine/SCS_Node.h"
|
|
#include "EdGraph/EdGraph.h"
|
|
#include "EdGraph/EdGraphNode.h"
|
|
#include "EdGraph/EdGraphPin.h"
|
|
#include "K2Node_EditablePinBase.h"
|
|
#include "EdGraph/EdGraphSchema.h"
|
|
#include "Kismet2/BlueprintEditorUtils.h"
|
|
#include "Kismet2/Kismet2NameValidators.h"
|
|
#include "Kismet2/KismetEditorUtilities.h"
|
|
#include "UObject/UObjectIterator.h"
|
|
#include "UObject/UnrealType.h"
|
|
#include "Misc/Paths.h"
|
|
#include "Misc/PackageName.h"
|
|
|
|
// Reparent validation
|
|
#include "Engine/LevelScriptActor.h"
|
|
#include "Animation/AnimInstance.h"
|
|
#include "Blueprint/UserWidget.h"
|
|
|
|
// Animation Blueprint support
|
|
#include "AnimStateNode.h"
|
|
#include "AnimStateTransitionNode.h"
|
|
#include "AnimationStateMachineGraph.h"
|
|
|
|
// Material support
|
|
#include "Materials/Material.h"
|
|
#include "Materials/MaterialExpression.h"
|
|
#include "Materials/MaterialFunction.h"
|
|
#include "Materials/MaterialInstanceConstant.h"
|
|
#include "MaterialGraph/MaterialGraph.h"
|
|
#include "MaterialGraph/MaterialGraphSchema.h"
|
|
#include "IMaterialEditor.h"
|
|
#include "Subsystems/AssetEditorSubsystem.h"
|
|
|
|
// Mesh, animation, texture support
|
|
#include "Engine/StaticMesh.h"
|
|
#include "Engine/SkeletalMesh.h"
|
|
#include "Animation/AnimSequence.h"
|
|
#include "Animation/BlendSpace.h"
|
|
#include "Engine/Texture.h"
|
|
|
|
|
|
// ============================================================
|
|
// Name sanitization
|
|
// ============================================================
|
|
|
|
FString WingUtils::SanitizeName(const FString &InName)
|
|
{
|
|
FString Name = InName;
|
|
int32 Dst = 0;
|
|
for (int32 Src = 0; Src < Name.Len(); Src++)
|
|
{
|
|
TCHAR c = Name[Src];
|
|
if (c < 0x20 || c == 0x7F) continue;
|
|
if (c == ' ') c=L'·';
|
|
if (c == '<') c=L'◁';
|
|
if (c == '>') c=L'▷';
|
|
if (c == ',') c=L'▾';
|
|
Name[Dst++] = c;
|
|
}
|
|
if (Dst == 0) Name[Dst++] = L'·';
|
|
Name.LeftInline(Dst);
|
|
return Name;
|
|
}
|
|
|
|
FString WingUtils::UnsanitizeName(const FString &InName)
|
|
{
|
|
FString Name = InName;
|
|
int32 Dst = 0;
|
|
for (int32 Src = 0; Src < Name.Len(); Src++)
|
|
{
|
|
TCHAR c = Name[Src];
|
|
if (c < 0x20 || c == 0x7F) continue;
|
|
if (c == L'·') c=' ';
|
|
if (c == L'◁') c='<';
|
|
if (c == L'▷') c='>';
|
|
if (c == L'▾') c=',';
|
|
Name[Dst++] = c;
|
|
}
|
|
Name.LeftInline(Dst);
|
|
return Name;
|
|
}
|
|
|
|
FString WingUtils::SanitizeName(FName Name)
|
|
{
|
|
return SanitizeName(Name.ToString());
|
|
}
|
|
|
|
FString WingUtils::StandardizeMenuItem(const FString &Item)
|
|
{
|
|
FString Sanitized = Item;
|
|
int32 Dst = 0;
|
|
bool Upper = true;
|
|
for (int32 Src = 0; Src < Sanitized.Len(); Src++)
|
|
{
|
|
TCHAR c = Sanitized[Src];
|
|
if (FChar::IsAlnum(c))
|
|
{
|
|
if (Upper) c = FChar::ToUpper(c);
|
|
Sanitized[Dst++] = c;
|
|
Upper = false;
|
|
}
|
|
else
|
|
{
|
|
Upper = true;
|
|
if ((c <= 0x20)||(c == 0x7F)) continue;
|
|
if (c == ':') c = L'⁖';
|
|
Sanitized[Dst++] = c;
|
|
}
|
|
}
|
|
Sanitized.LeftInline(Dst);
|
|
return Sanitized;
|
|
}
|
|
|
|
FString WingUtils::CheckProposedName(const FString &Name)
|
|
{
|
|
FString Unsanitized = UnsanitizeName(Name);
|
|
if ((Unsanitized.IsEmpty()) || (SanitizeName(Unsanitized) != Name))
|
|
{
|
|
UWingServer::Printf(TEXT("Names must not contain control characters or be empty\n"));
|
|
return FString();
|
|
}
|
|
return Unsanitized;
|
|
}
|
|
|
|
// ============================================================
|
|
// Name Lookup
|
|
// ============================================================
|
|
|
|
FString WingUtils::FormatName(const UWorld *World)
|
|
{
|
|
return World->GetPathName();
|
|
}
|
|
|
|
FString WingUtils::FormatName(const UBlueprint *BP)
|
|
{
|
|
return UWingTypes::TypeToTextOrDie(BP);
|
|
}
|
|
|
|
FString WingUtils::FormatName(const UActorComponent *C)
|
|
{
|
|
return SanitizeName(C->GetName());
|
|
}
|
|
|
|
FString WingUtils::FormatName(const USCS_Node *Node)
|
|
{
|
|
return SanitizeName(Node->GetVariableName());
|
|
}
|
|
|
|
FString WingUtils::FormatName(const UEdGraph *Graph)
|
|
{
|
|
return SanitizeName(Graph->GetName());
|
|
}
|
|
|
|
FString WingUtils::FormatName(const TObjectPtr<UEdGraph> &Graph)
|
|
{
|
|
return SanitizeName(Graph->GetName());
|
|
}
|
|
|
|
FString WingUtils::FormatName(const UEdGraphNode* Node)
|
|
{
|
|
return SanitizeName(Node->GetName());
|
|
}
|
|
|
|
FString WingUtils::FormatName(const UEdGraphPin *Pin)
|
|
{
|
|
return SanitizeName(Pin->GetName());
|
|
}
|
|
|
|
FString WingUtils::FormatName(const FMemberReference &Ref)
|
|
{
|
|
return SanitizeName(Ref.GetMemberName());
|
|
}
|
|
|
|
FString WingUtils::FormatName(const FBPVariableDescription &Var)
|
|
{
|
|
return SanitizeName(Var.VarName);
|
|
}
|
|
|
|
FString WingUtils::FormatName(const UStruct *Struct)
|
|
{
|
|
if (Cast<UScriptStruct>(Struct) || Cast<UClass>(Struct))
|
|
return UWingTypes::TypeToTextOrDie(Struct);
|
|
return SanitizeName(Struct->GetName());
|
|
}
|
|
|
|
FString WingUtils::FormatName(const UClass *Class)
|
|
{
|
|
return UWingTypes::TypeToTextOrDie(Class);
|
|
}
|
|
|
|
FString WingUtils::FormatName(const UMaterial *Material)
|
|
{
|
|
return Material->GetPathName();
|
|
}
|
|
|
|
FString WingUtils::FormatName(const UMaterialInstance *MaterialInstance)
|
|
{
|
|
return MaterialInstance->GetPathName();
|
|
}
|
|
|
|
FString WingUtils::FormatName(const UMaterialFunction *MaterialFunction)
|
|
{
|
|
return MaterialFunction->GetPathName();
|
|
}
|
|
|
|
FString WingUtils::FormatName(const UMaterialExpression *Expression)
|
|
{
|
|
return SanitizeName(Expression->GetName());
|
|
}
|
|
|
|
FString WingUtils::FormatName(const UStaticMesh *Mesh)
|
|
{
|
|
return Mesh->GetPathName();
|
|
}
|
|
|
|
FString WingUtils::FormatName(const USkeletalMesh *Mesh)
|
|
{
|
|
return Mesh->GetPathName();
|
|
}
|
|
|
|
FString WingUtils::FormatName(const UAnimSequence *Anim)
|
|
{
|
|
return Anim->GetPathName();
|
|
}
|
|
|
|
FString WingUtils::FormatName(const UBlendSpace *BlendSpace)
|
|
{
|
|
return BlendSpace->GetPathName();
|
|
}
|
|
|
|
FString WingUtils::FormatName(const UTexture *Texture)
|
|
{
|
|
return Texture->GetPathName();
|
|
}
|
|
|
|
FString WingUtils::FormatName(const UScriptStruct *Struct)
|
|
{
|
|
return UWingTypes::TypeToTextOrDie(Struct);
|
|
}
|
|
|
|
FString WingUtils::FormatName(const UEnum *Enum)
|
|
{
|
|
return UWingTypes::TypeToTextOrDie(Enum);
|
|
}
|
|
|
|
FString WingUtils::FormatName(const FProperty *Prop)
|
|
{
|
|
return SanitizeName(Prop->GetName());
|
|
}
|
|
|
|
FString WingUtils::FormatName(const FUserPinInfo &Pin)
|
|
{
|
|
return SanitizeName(Pin.PinName);
|
|
}
|
|
|
|
FString WingUtils::FormatName(const FBPInterfaceDescription &IFace)
|
|
{
|
|
return FormatName(IFace.Interface);
|
|
}
|
|
|
|
FString WingUtils::FormatName(const FWingActorComponent &Comp)
|
|
{
|
|
return SanitizeName(Comp.GetName());
|
|
}
|
|
|
|
FString WingUtils::FormatName(const UWidget *Widget)
|
|
{
|
|
return SanitizeName(Widget->GetName());
|
|
}
|
|
|
|
// ============================================================
|
|
// Formatting other things
|
|
// ============================================================
|
|
|
|
FString WingUtils::FormatNodeTitle(const UEdGraphNode *Node)
|
|
{
|
|
FString Title = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
|
int32 NewlineIdx;
|
|
if (Title.FindChar(TEXT('\n'), NewlineIdx))
|
|
Title.LeftInline(NewlineIdx);
|
|
return Title;
|
|
}
|
|
|
|
// ============================================================
|
|
// JSON helpers
|
|
// ============================================================
|
|
|
|
// ============================================================
|
|
// Text formatting
|
|
// ============================================================
|
|
|
|
FString WingUtils::WrapText(const FString& Text, int32 ColLimit, const FString& Prefix)
|
|
{
|
|
FString Clean = Text;
|
|
Clean.ReplaceInline(TEXT("\r\n"), TEXT("\n"));
|
|
TArray<FString> Words;
|
|
Clean.ParseIntoArrayWS(Words);
|
|
|
|
TStringBuilder<1024> Result;
|
|
int32 Col = 0;
|
|
for (const FString& Word : Words)
|
|
{
|
|
if (Col > 0 && Col + 1 + Word.Len() > ColLimit)
|
|
{
|
|
Result.Append(TEXT("\n"));
|
|
Col = 0;
|
|
}
|
|
if (Col == 0)
|
|
{
|
|
Result.Append(Prefix);
|
|
Col = Prefix.Len();
|
|
}
|
|
else
|
|
{
|
|
Result.Append(TEXT(" "));
|
|
Col += 1;
|
|
}
|
|
Result.Append(Word);
|
|
Col += Word.Len();
|
|
}
|
|
return Result.ToString();
|
|
}
|
|
|
|
// ============================================================
|
|
// Enum helpers
|
|
// ============================================================
|
|
|
|
FString WingUtils::EnumToString(UEnum* Enum, int64 Value)
|
|
{
|
|
return Enum->GetNameStringByValue(Value);
|
|
}
|
|
|
|
bool WingUtils::StringToEnum(UEnum* Enum, const FString& Str, int64& OutValue)
|
|
{
|
|
int32 Index = Enum->GetIndexByNameString(Str);
|
|
if (Index == INDEX_NONE)
|
|
{
|
|
FString Prefix = Enum->GenerateEnumPrefix();
|
|
if (!Prefix.IsEmpty())
|
|
Index = Enum->GetIndexByNameString(Prefix + TEXT("_") + Str);
|
|
}
|
|
if (Index == INDEX_NONE)
|
|
{
|
|
UWingServer::Printf(TEXT("ERROR: '%s' is not a valid value for %s\n"), *Str, *Enum->GetName());
|
|
OutValue = 0;
|
|
return false;
|
|
}
|
|
OutValue = Enum->GetValueByIndex(Index);
|
|
return true;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// Common Error Reporting
|
|
// ============================================================
|
|
|
|
bool WingUtils::CheckExactlyOneNamed(int Count, const TCHAR *Kind, const FString &Name)
|
|
{
|
|
if (Count == 0)
|
|
{
|
|
UWingServer::Printf(TEXT("Could not find a %s named %s.\n"), Kind, *Name);
|
|
return false;
|
|
}
|
|
if (Count > 1)
|
|
{
|
|
UWingServer::Printf(TEXT("More than one %s named %s\n"), Kind, *Name);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool WingUtils::CheckExactlyNoneNamed(int Count, const TCHAR *Kind, const FString &Name)
|
|
{
|
|
if (Count > 0)
|
|
{
|
|
UWingServer::Printf(TEXT("A %s named %s already exists."), Kind, *Name);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool WingUtils::CheckCanRename(UEdGraphNode* Node, const FString &Name)
|
|
{
|
|
if (!Node->bCanRenameNode)
|
|
{
|
|
UWingServer::Printf(TEXT("ERROR: Node %s does not support renaming.\n"), *FormatName(Node));
|
|
return false;
|
|
}
|
|
TSharedPtr<INameValidatorInterface> Validator = FNameValidatorFactory::MakeValidator(Node);
|
|
EValidatorResult Result = Validator->IsValid(Name, false);
|
|
if (Result != EValidatorResult::Ok && Result != EValidatorResult::ExistingName)
|
|
{
|
|
UWingServer::Printf(TEXT("ERROR: %s\n"), *INameValidatorInterface::GetErrorString(Name, Result));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// ============================================================
|
|
// Reparent validation
|
|
// ============================================================
|
|
|
|
bool WingUtils::CanReparentBlueprint(UClass* CurrentGenerated, UClass* Proposed)
|
|
{
|
|
if (!CurrentGenerated || !Proposed) return false;
|
|
|
|
UClass* CurrentParent = CurrentGenerated->GetSuperClass();
|
|
if (!CurrentParent) return false;
|
|
|
|
// Don't allow parenting to itself or one of its children
|
|
if (Proposed->IsChildOf(CurrentGenerated)) return false;
|
|
|
|
// Don't allow parenting to an interface
|
|
if (Proposed->IsChildOf(UInterface::StaticClass())) return false;
|
|
|
|
// LevelScriptActor blueprints stay in the LevelScriptActor hierarchy
|
|
if (CurrentParent->IsChildOf(ALevelScriptActor::StaticClass()))
|
|
return Proposed->IsChildOf(ALevelScriptActor::StaticClass());
|
|
|
|
// Actor blueprints stay in the Actor hierarchy, but not LevelScriptActor
|
|
if (CurrentParent->IsChildOf(AActor::StaticClass()))
|
|
return Proposed->IsChildOf(AActor::StaticClass()) &&
|
|
!Proposed->IsChildOf(ALevelScriptActor::StaticClass());
|
|
|
|
// Component blueprints stay in the ActorComponent hierarchy
|
|
if (CurrentParent->IsChildOf(UActorComponent::StaticClass()))
|
|
return Proposed->IsChildOf(UActorComponent::StaticClass());
|
|
|
|
// Anim blueprints stay in the AnimInstance hierarchy
|
|
if (CurrentParent->IsChildOf(UAnimInstance::StaticClass()))
|
|
return Proposed->IsChildOf(UAnimInstance::StaticClass());
|
|
|
|
// Widget blueprints stay in the UserWidget hierarchy
|
|
if (CurrentParent->IsChildOf(UUserWidget::StaticClass()))
|
|
return Proposed->IsChildOf(UUserWidget::StaticClass());
|
|
|
|
return false;
|
|
}
|
|
|
|
// ============================================================
|
|
// Blueprint helpers
|
|
// ============================================================
|
|
|
|
TArray<UEdGraph*> WingUtils::AllGraphs(UBlueprint* BP)
|
|
{
|
|
TArray<UEdGraph*> Graphs;
|
|
BP->GetAllGraphs(Graphs);
|
|
return Graphs;
|
|
}
|
|
|
|
TArray<UEdGraphNode*> WingUtils::AllNodes(UBlueprint* BP)
|
|
{
|
|
TArray<UEdGraphNode*> Nodes;
|
|
for (UEdGraph* Graph : AllGraphs(BP))
|
|
Nodes.Append(Graph->Nodes);
|
|
return Nodes;
|
|
}
|
|
|
|
TArray<UEdGraphNode*> WingUtils::AllNodes(UEdGraph *Graph)
|
|
{
|
|
TArray<UEdGraphNode*> Result;
|
|
Result.Append(Graph->Nodes);
|
|
return Result;
|
|
}
|
|
|
|
// ============================================================
|
|
// Material helpers
|
|
// ============================================================
|
|
|
|
void WingUtils::EnsureMaterialGraph(UMaterial* Material)
|
|
{
|
|
if (!Material) return;
|
|
if (!Material->MaterialGraph)
|
|
{
|
|
// In commandlet/headless mode the MaterialGraph is not auto-created.
|
|
// Replicate what the Material Editor does on open (MaterialEditor.cpp:619).
|
|
Material->MaterialGraph = CastChecked<UMaterialGraph>(
|
|
FBlueprintEditorUtils::CreateNewGraph(
|
|
Material, NAME_None,
|
|
UMaterialGraph::StaticClass(),
|
|
UMaterialGraphSchema::StaticClass()));
|
|
Material->MaterialGraph->Material = Material;
|
|
Material->MaterialGraph->RebuildGraph();
|
|
}
|
|
}
|
|
|
|
UMaterial* WingUtils::ReplaceMaterialWithTransientCopy(UMaterial* Material)
|
|
{
|
|
if (!Material) return nullptr;
|
|
|
|
// Already a preview material — nothing to do.
|
|
if (Material->GetOutermost() == GetTransientPackage())
|
|
return Material;
|
|
|
|
// If the material editor has a transient preview copy open, get it
|
|
// via the editor API. This follows the same pattern as Epic's
|
|
// MaterialEditingLibrary (FindMaterialEditorForAsset).
|
|
UAssetEditorSubsystem* Sub = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
|
|
IAssetEditorInstance* EditorInstance = Sub ? Sub->FindEditorForAsset(Material, false) : nullptr;
|
|
if (EditorInstance)
|
|
{
|
|
// This is a weird hack. We know that the IAssetEditorInstance for a material
|
|
// is always going to be an FMaterialEditor, which conforms to IMaterialEditor.
|
|
// If that weren't the case, this unsafe code would crash hard. However,
|
|
// lots of places in unreal use this same unsafe pattern.
|
|
IMaterialEditor* MatEditor = static_cast<IMaterialEditor*>(EditorInstance);
|
|
UMaterialInterface* Edited = MatEditor->GetMaterialInterface();
|
|
if (UMaterial* EditedMat = Cast<UMaterial>(Edited))
|
|
return EditedMat;
|
|
}
|
|
|
|
return Material;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// Anim blueprint helpers
|
|
// ============================================================
|
|
|
|
UAnimationStateMachineGraph* WingUtils::FindStateMachineGraph(UBlueprint* BP, const FString& GraphName)
|
|
{
|
|
TArray<UEdGraph*> AllGraphs;
|
|
BP->GetAllGraphs(AllGraphs);
|
|
for (UEdGraph* Graph : AllGraphs)
|
|
{
|
|
if (UAnimationStateMachineGraph* SMGraph = Cast<UAnimationStateMachineGraph>(Graph))
|
|
{
|
|
if (SMGraph->GetName() == GraphName)
|
|
{
|
|
return SMGraph;
|
|
}
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
UAnimStateNode* WingUtils::FindStateByName(UAnimationStateMachineGraph* SMGraph, const FString& StateName)
|
|
{
|
|
for (UEdGraphNode* Node : SMGraph->Nodes)
|
|
{
|
|
if (UAnimStateNode* StateNode = Cast<UAnimStateNode>(Node))
|
|
{
|
|
if (StateNode->GetStateName() == StateName)
|
|
{
|
|
return StateNode;
|
|
}
|
|
}
|
|
}
|
|
UWingServer::Printf(TEXT("ERROR: State '%s' not found in graph '%s'\n"), *StateName, *SMGraph->GetName());
|
|
return nullptr;
|
|
}
|
|
|
|
UAnimStateTransitionNode* WingUtils::FindTransition(UAnimationStateMachineGraph* SMGraph,
|
|
const FString& FromStateName, const FString& ToStateName)
|
|
{
|
|
for (UEdGraphNode* Node : SMGraph->Nodes)
|
|
{
|
|
if (UAnimStateTransitionNode* TransNode = Cast<UAnimStateTransitionNode>(Node))
|
|
{
|
|
UAnimStateNode* FromState = Cast<UAnimStateNode>(TransNode->GetPreviousState());
|
|
UAnimStateNode* ToState = Cast<UAnimStateNode>(TransNode->GetNextState());
|
|
if (FromState && ToState &&
|
|
(FromState->GetStateName() == FromStateName) &&
|
|
(ToState->GetStateName() == ToStateName))
|
|
{
|
|
return TransNode;
|
|
}
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// ============================================================
|
|
// Support for locating UE Wingman Handlers
|
|
// ============================================================
|
|
|
|
TArray<UClass*> WingUtils::CollectHandlerClasses()
|
|
{
|
|
TArray<UClass*> Result;
|
|
for (TObjectIterator<UClass> It; It; ++It)
|
|
{
|
|
UClass* Class = *It;
|
|
if (Class->HasAnyClassFlags(CLASS_Abstract)) continue;
|
|
if (!Class->ImplementsInterface(UWingHandler::StaticClass())) continue;
|
|
Result.Add(Class);
|
|
}
|
|
Result.Sort([](UClass& A, UClass& B) { return GetHandlerName(&A) < GetHandlerName(&B); });
|
|
return Result;
|
|
}
|
|
|
|
FString WingUtils::GetHandlerName(UClass* HandlerClass)
|
|
{
|
|
FString Name = HandlerClass->GetName();
|
|
Name.RemoveFromStart(TEXT("Wing_"));
|
|
return Name;
|
|
}
|
|
|
|
FString WingUtils::GetHandlerGroup(UClass* HandlerClass)
|
|
{
|
|
FString Name = HandlerClass->GetName();
|
|
Name.RemoveFromStart(TEXT("Wing_"));
|
|
// Everything before the underscore is the group
|
|
int32 UnderscoreIdx;
|
|
if (Name.FindChar(TEXT('_'), UnderscoreIdx))
|
|
return Name.Left(UnderscoreIdx);
|
|
return Name;
|
|
}
|
|
|
|
// ============================================================
|
|
// PrintHandlerHelp — verbose description of one handler command
|
|
// ============================================================
|
|
|
|
void WingUtils::PrintHandlerHelp(UClass* HandlerClass)
|
|
{
|
|
const IWingHandler* Handler = Cast<IWingHandler>(HandlerClass->GetDefaultObject());
|
|
if (!Handler) return;
|
|
|
|
FString ToolName = GetHandlerName(HandlerClass);
|
|
|
|
UWingServer::Print(TEXT("\n"));
|
|
UWingServer::Print(WrapText(Handler->GetDescription(), 80, TEXT("// ")));
|
|
UWingServer::Print(TEXT("\n"));
|
|
|
|
// Command signature line
|
|
UWingServer::Print(ToolName);
|
|
UWingServer::Print(TEXT("("));
|
|
bool bFirst = true;
|
|
for (TFieldIterator<FProperty> PropIt(HandlerClass, EFieldIterationFlags::None); PropIt; ++PropIt)
|
|
{
|
|
if (!bFirst) UWingServer::Print(TEXT(","));
|
|
bFirst = false;
|
|
if (PropIt->HasMetaData(TEXT("Optional"))) UWingServer::Print(TEXT("?"));
|
|
UWingServer::Print(PropIt->GetName());
|
|
}
|
|
UWingServer::Print(TEXT(")\n"));
|
|
|
|
// parameter details
|
|
for (TFieldIterator<FProperty> PropIt(HandlerClass, EFieldIterationFlags::None); PropIt; ++PropIt)
|
|
{
|
|
FProperty* Prop = *PropIt;
|
|
FString Name = Prop->GetName();
|
|
FString Type = UWingTypes::TypeToText(Prop);
|
|
bool bOptional = Prop->HasMetaData(TEXT("Optional"));
|
|
const FString& Desc = Prop->GetMetaData(TEXT("Description"));
|
|
|
|
UWingServer::Printf(TEXT(" %s %s%s"),
|
|
*Type, *Name, bOptional ? TEXT(" (optional)") : TEXT(""));
|
|
if (!Desc.IsEmpty())
|
|
UWingServer::Printf(TEXT(" — %s"), *Desc);
|
|
UWingServer::Print(TEXT("\n"));
|
|
}
|
|
}
|