diff --git a/Source/CommonValidators/CommonValidators.Build.cs b/Source/CommonValidators/CommonValidators.Build.cs index b2fb9c8..4bba345 100644 --- a/Source/CommonValidators/CommonValidators.Build.cs +++ b/Source/CommonValidators/CommonValidators.Build.cs @@ -15,6 +15,7 @@ public CommonValidators(ReadOnlyTargetRules Target) : base(Target) "DeveloperSettings", "Kismet", "UnrealEd", + "ApplicationCore", "AssetManagerEditor" }); } diff --git a/Source/CommonValidators/CommonValidatorsDeveloperSettings.h b/Source/CommonValidators/CommonValidatorsDeveloperSettings.h index b55a92f..af566ba 100644 --- a/Source/CommonValidators/CommonValidatorsDeveloperSettings.h +++ b/Source/CommonValidators/CommonValidatorsDeveloperSettings.h @@ -28,6 +28,95 @@ struct FCommonValidatorClassArray } }; + +/** Configurable severity for settings-driven Blueprint validation rules. */ +UENUM(BlueprintType) +enum class EEditorValidatorBlueprintValidationRuleSeverity : uint8 +{ + Info, + PerformanceWarning, + Warning, + Error +}; + +/** Controls whether a base Blueprint class is included when matching a validation rule. */ +UENUM(BlueprintType) +enum class EEditorValidatorBlueprintBaseScope : uint8 +{ + BaseAndDerived, + DerivedOnly +}; + +/** Settings-driven rule for forbidding Blueprint implementations. */ +USTRUCT(BlueprintType) +struct FEditorValidatorBlueprintImplementationRule +{ + GENERATED_BODY() + + /** + * Base class this rule applies to. + * Any Blueprint inheriting from this class may be validated. + */ + UPROPERTY(EditAnywhere, Config, Category="Implementation", meta=(ToolTip="The Blueprint base class this rule applies to.")) + TSoftClassPtr BlueprintBaseClass; + + /** + * Determines whether validation applies to the selected class itself, + * or only Blueprints derived from it. + */ + UPROPERTY(EditAnywhere, Config, Category="Implementation", meta=(ToolTip="Whether the selected class itself or only derived classes are checked.")) + EEditorValidatorBlueprintBaseScope BaseScope = EEditorValidatorBlueprintBaseScope::DerivedOnly; + + /** + * Blueprint function or event that should not be overridden. + * + * Uses the Blueprint-visible name, not necessarily the native C++ name. + * + * Example: + * Native: ActivateAbility() + * Blueprint: K2_ActivateAbility + */ + UPROPERTY(EditAnywhere, Config, Category="Implementation", meta=(ToolTip="The Blueprint function or event to forbid. Use the Blueprint-visible name.")) + FName FunctionToForbid; + + /** + * Validation message shown when this rule is triggered. + * + * Example: + * 'Do not override ActivateAbility directly.' + */ + UPROPERTY(EditAnywhere, Config, Category="Message", meta=(MultiLine="true", ToolTip="Message shown when validation fails.")) + FText Message; + + /** + * Suggested fix shown alongside the validation message. + * + * Example: + * 'Move gameplay logic into OnActionStarted instead.' + */ + UPROPERTY(EditAnywhere, Config, Category="Message", meta=(MultiLine="true", ToolTip="Suggested fix or alternative workflow.")) + FString Suggestion; + + /** + * Determines how severe the validation issue appears in the editor. + * + * Warning: + * General issue that should be fixed. + * + * Error: + * Rule violation that should block usage. + * + * PerformanceWarning: + * Issue related to runtime or scalability concerns. + * + * Info: + * Non-blocking informational message. + */ + UPROPERTY(EditAnywhere, Config, Category="Message", meta=(ToolTip="Controls how severe this validation issue appears.")) + EEditorValidatorBlueprintValidationRuleSeverity Severity = + EEditorValidatorBlueprintValidationRuleSeverity::Warning; +}; + UCLASS(config = Editor, defaultconfig, meta = (DisplayName = "Common Validators")) class COMMONVALIDATORS_API UCommonValidatorsDeveloperSettings : public UDeveloperSettings { @@ -126,4 +215,6 @@ class COMMONVALIDATORS_API UCommonValidatorsDeveloperSettings : public UDevelope UPROPERTY(Config, EditAnywhere, Category="Common Validators|Material Texture Sampler Validator") bool bEnableMaterialTextureSamplerValidator = true; + UPROPERTY(EditAnywhere, Config, Category = "Blueprint Implementations") + TArray BlueprintImplementationRules; }; \ No newline at end of file diff --git a/Source/CommonValidators/EditorValidator_BlueprintRestriction.cpp b/Source/CommonValidators/EditorValidator_BlueprintRestriction.cpp new file mode 100644 index 0000000..f63462d --- /dev/null +++ b/Source/CommonValidators/EditorValidator_BlueprintRestriction.cpp @@ -0,0 +1,402 @@ +#include "EditorValidator_BlueprintRestriction.h" +#include "CommonValidatorsStatics.h" +#include "CommonValidatorsDeveloperSettings.h" +#include "EdGraph/EdGraph.h" +#include "HAL/PlatformApplicationMisc.h" +#include "Misc/DataValidation.h" + +FEditorValidatorBlueprintNodeRuleBuilder UEditorValidator_BlueprintRestriction::FORBID_OVERRIDE(const FName FunctionName) const +{ + FEditorValidatorBlueprintNodeRuleBuilder Builder; + + Builder.Rule.Matches = [FunctionName](UEdGraphNode* Node, const UBlueprint*) + { + return IsOverrideOf(Node, FunctionName); + }; + + return Builder; +} + +FEditorValidatorBlueprintCallRuleBuilder UEditorValidator_BlueprintRestriction::FORBID_CALL(UClass* OwnerClass, const FName FunctionName) const +{ + FEditorValidatorBlueprintCallRuleBuilder Builder; + + Builder.Rule.OwnerRootClass = OwnerClass; + Builder.Rule.ForbiddenFunction = FunctionName; + + return Builder; +} + +bool UEditorValidator_BlueprintRestriction::IsOverrideOf(UEdGraphNode* Node, const FName FunctionName) +{ + if (UK2Node_Event* Event = Cast(Node)) + { + if (IsGhostEvent(Event)) return false; + return Event->GetFunctionName() == FunctionName; + } + + if (UK2Node_FunctionEntry* Entry = Cast(Node)) + { + return Entry->GetGraph()->GetFName() == FunctionName; + } + + return false; +} + +bool UEditorValidator_BlueprintRestriction::IsGhostEvent(const UK2Node_Event* EventNode) +{ + if (!EventNode) return false; + + const auto State = EventNode->GetDesiredEnabledState(); + return State == ENodeEnabledState::Disabled + || State == ENodeEnabledState::DevelopmentOnly; +} + +bool UEditorValidator_BlueprintRestriction::TryGetCallFunctionInfo( + UEdGraphNode* Node, + UClass*& OutOwnerClass, + FName& OutNormalized, + FString& OutWhatUsed) +{ + const UK2Node_CallFunction* Call = Cast(Node); + if (!Call) return false; + + const UFunction* Func = Call->GetTargetFunction(); + if (!Func) return false; + + OutOwnerClass = Func->GetOuterUClass(); + OutWhatUsed = Func->GetName(); + + FString Name = Func->GetName(); + Name.RemoveFromStart(TEXT("K2_")); + Name.RemoveFromStart(TEXT("BP_")); + OutNormalized = FName(*Name); + + return true; +} + +void UEditorValidator_BlueprintRestriction::ValidateBlueprint( + UBlueprint* Blueprint, + FDataValidationContext& Context, + bool& bAnyViolation) const +{ + TArray NodeRules; + TArray CallRules; + + BuildNodeRules(NodeRules); + BuildCallRules(CallRules); + + TArray Graphs; + Graphs.Append(Blueprint->FunctionGraphs); + Graphs.Append(Blueprint->UbergraphPages); + + for (UEdGraph* Graph : Graphs) + { + if (!Graph) continue; + + for (UEdGraphNode* Node : Graph->Nodes) + { + if (!Node) continue; + + for (const auto& Rule : NodeRules) + { + if (!Rule.Matches || !Rule.Matches(Node, Blueprint)) + continue; + + TArray Suggestions; + if (Rule.BuildSuggestions) + { + Rule.BuildSuggestions(Node, Suggestions); + } + + const EMessageSeverity::Type Severity = + Rule.GetSeverity ? Rule.GetSeverity(Blueprint) : EMessageSeverity::Warning; + + AddViolationMessage( + Context, + Blueprint, + Graph, + Node, + Severity, + Rule.RuleText, + Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString(), + Suggestions + ); + + if (Severity == EMessageSeverity::Error || Severity == EMessageSeverity::Warning) + { + bAnyViolation = true; + } + break; + } + + UClass* Owner = nullptr; + FName Normalized = NAME_None; + FString WhatUsed; + + if (!TryGetCallFunctionInfo(Node, Owner, Normalized, WhatUsed)) + continue; + + for (const auto& Rule : CallRules) + { + if (Rule.Applies && !Rule.Applies(Blueprint)) + continue; + + if (!Rule.OwnerRootClass || !Owner->IsChildOf(Rule.OwnerRootClass)) + continue; + + if (Rule.ForbiddenFunction != Normalized) + continue; + + TArray Suggestions; + if (Rule.BuildSuggestions) + { + Rule.BuildSuggestions(Normalized, Suggestions); + } + + const EMessageSeverity::Type Severity = + Rule.GetSeverity ? Rule.GetSeverity(Blueprint) : EMessageSeverity::Warning; + + AddViolationMessage( + Context, + Blueprint, + Graph, + Node, + Severity, + Rule.RuleText, + WhatUsed, + Suggestions + ); + + if (Severity == EMessageSeverity::Error || Severity == EMessageSeverity::Warning) + { + bAnyViolation = true; + } + break; + } + } + } +} + +void UEditorValidator_BlueprintRestriction::AddViolationMessage( + FDataValidationContext& Context, + UBlueprint* Blueprint, + UEdGraph* Graph, + UEdGraphNode* Node, + EMessageSeverity::Type Severity, + const FText& RuleText, + const FString& WhatUsed, + const TArray& Suggestions) +{ + const FString BlueprintName = Blueprint ? Blueprint->GetName() : TEXT(""); + const FString GraphName = Graph ? Graph->GetName() : TEXT(""); + const FString NodeTitle = Node ? Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString() : TEXT(""); + + const FString Body = FString::Printf( + TEXT("%s\n\nYou used: %s\nLocation: %s -> %s -> %s"), + *RuleText.ToString(), + *WhatUsed, + *BlueprintName, + *GraphName, + *NodeTitle + ); + + TSharedRef Message = + FTokenizedMessage::Create(Severity, FText::FromString(Body)); + + const TWeakObjectPtr WeakBlueprint(Blueprint); + const TWeakObjectPtr WeakGraph(Graph); + const FGuid NodeGuid = Node ? Node->NodeGuid : FGuid(); + + Message->AddToken(FActionToken::Create( + FText::FromString(TEXT("Open Blueprint")), + FText::FromString(TEXT("Open the Blueprint and focus this node")), + FOnActionTokenExecuted::CreateLambda([WeakBlueprint, WeakGraph, NodeGuid]() + { + if (!WeakBlueprint.IsValid() || !WeakGraph.IsValid()) + return; + + for (UEdGraphNode* N : WeakGraph->Nodes) + { + if (N && N->NodeGuid == NodeGuid) + { + UCommonValidatorsStatics::OpenBlueprintAndFocusNode( + WeakBlueprint.Get(), + WeakGraph.Get(), + N + ); + break; + } + } + }), + false + )); + + for (const auto& S : Suggestions) + { + if (S.OnExecute) + { + Message->AddToken(FActionToken::Create( + S.DisplayText, + FText::FromString(TEXT("Execute action")), + FOnActionTokenExecuted::CreateLambda([S]() + { + S.OnExecute(); + }), + false + )); + } + else if (S.bAllowCopy && S.CopyText.IsSet()) + { + const FString Copy = S.CopyText.GetValue(); + + Message->AddToken(FActionToken::Create( + S.DisplayText, + FText::FromString(TEXT("Copy to clipboard")), + FOnActionTokenExecuted::CreateLambda([Copy]() + { + FPlatformApplicationMisc::ClipboardCopy(*Copy); + }), + false + )); + } + else + { + Message->AddToken(FTextToken::Create(S.DisplayText)); + } + } + + Context.AddMessage(Message); +} + +bool UEditorValidator_BlueprintRestriction::CanValidateAsset_Implementation( + const FAssetData& InAssetData, + UObject* InObject, + FDataValidationContext& InContext) const +{ + return Super::CanValidateAsset_Implementation(InAssetData, InObject, InContext) + && AppliesTo(InObject); +} + +EDataValidationResult UEditorValidator_BlueprintRestriction::ValidateLoadedAsset_Implementation( + const FAssetData&, + UObject* InAsset, + FDataValidationContext& Context) +{ + bool bAnyViolation = false; + + if (UBlueprint* BP = Cast(InAsset)) + { + ValidateBlueprint(BP, Context, bAnyViolation); + } + + return bAnyViolation ? EDataValidationResult::Invalid : EDataValidationResult::Valid; +} + +static EMessageSeverity::Type ToMessageSeverity( + EEditorValidatorBlueprintValidationRuleSeverity In) +{ + switch (In) + { + case EEditorValidatorBlueprintValidationRuleSeverity::Info: + return EMessageSeverity::Info; + case EEditorValidatorBlueprintValidationRuleSeverity::PerformanceWarning: + return EMessageSeverity::PerformanceWarning; + case EEditorValidatorBlueprintValidationRuleSeverity::Error: + return EMessageSeverity::Error; + case EEditorValidatorBlueprintValidationRuleSeverity::Warning: + default: + return EMessageSeverity::Warning; + } +} + +static bool MatchesBaseScope( + const UBlueprint* BP, + UClass* BaseClass, + EEditorValidatorBlueprintBaseScope Scope) +{ + if (!BP || !BP->GeneratedClass || !BaseClass) + return false; + + if (Scope == EEditorValidatorBlueprintBaseScope::BaseAndDerived) + return BP->GeneratedClass->IsChildOf(BaseClass); + + return BP->GeneratedClass != BaseClass && + BP->GeneratedClass->IsChildOf(BaseClass); +} + +static bool IsBlueprintImplementationFunction(const UFunction* Func) +{ + return Func && + Func->HasAnyFunctionFlags(FUNC_Event | FUNC_BlueprintEvent); +} + +static FName ResolveBlueprintImplementationName( + UClass* ContextClass, + FName InputName) +{ + if (!ContextClass || InputName.IsNone()) + return InputName; + + const FString Desired = InputName.ToString(); + + for (TFieldIterator It(ContextClass, EFieldIteratorFlags::IncludeSuper); It; ++It) + { + const UFunction* Func = *It; + + if (!IsBlueprintImplementationFunction(Func)) + continue; + + if (Func->GetFName() == InputName) + return Func->GetFName(); + + if (Func->GetMetaData(TEXT("DisplayName")).Equals(Desired)) + return Func->GetFName(); + + if (FName::NameToDisplayString(Func->GetName(), false).Equals(Desired)) + return Func->GetFName(); + } + + return InputName; +} + +void UEditorValidator_BlueprintRestriction_SettingBased::BuildNodeRules( + TArray& OutRules) const +{ + const UCommonValidatorsDeveloperSettings* Settings = + GetDefault(); + + if (!Settings) + return; + + for (const FEditorValidatorBlueprintImplementationRule& Rule : Settings->BlueprintImplementationRules) + { + UClass* BaseClass = Rule.BlueprintBaseClass.LoadSynchronous(); + if (!BaseClass) + continue; + + const FName ResolvedFunctionName = + ResolveBlueprintImplementationName( + BaseClass, + Rule.FunctionToForbid + ); + + OutRules.Add( + FORBID_OVERRIDE(ResolvedFunctionName) + .Message(Rule.Message.ToString()) + .Suggest(Rule.Suggestion) + .When([Rule, BaseClass](const UBlueprint* BP) + { + return MatchesBaseScope( + BP, + BaseClass, + Rule.BaseScope + ); + }) + .CustomSeverity([Rule](const UBlueprint*) + { + return ToMessageSeverity(Rule.Severity); + }) + ); + } +} \ No newline at end of file diff --git a/Source/CommonValidators/EditorValidator_BlueprintRestriction.h b/Source/CommonValidators/EditorValidator_BlueprintRestriction.h new file mode 100644 index 0000000..f788d01 --- /dev/null +++ b/Source/CommonValidators/EditorValidator_BlueprintRestriction.h @@ -0,0 +1,416 @@ +#pragma once + +#include "CoreMinimal.h" +#include "EditorValidatorBase.h" +#include "K2Node_CallFunction.h" +#include "Logging/TokenizedMessage.h" +#include "K2Node_Event.h" +#include "K2Node_FunctionEntry.h" +#include "Engine/DeveloperSettings.h" +#include "EditorValidator_BlueprintRestriction.generated.h" + +#define UE_API COMMONVALIDATORS_API + +/* +Usage +----- + +1. Create a validator by deriving from UEditorValidator_BlueprintRestriction. + +2. Override AppliesTo() to restrict which assets the validator should scan. + +3. Override BuildNodeRules() to block Blueprint events, overrides, or custom node patterns. + +4. Override BuildCallRules() to block calls to specific functions. + +Example: + + virtual void BuildNodeRules(TArray& OutRules) const override + { + OutRules.Add( + FORBID_OVERRIDE(TEXT("ReceiveTick")) + .Message(TEXT("Blueprint Tick is not allowed.")) + .Suggest(TEXT("Use a timer or native C++ update path instead.")) + .Warning() + ); + + OutRules.Add( + FORBID_OVERRIDE(TEXT("K2_ActivateAbility")) + .When([](const UBlueprint* Blueprint) + { + return Blueprint + && Blueprint->GeneratedClass + && Blueprint->GeneratedClass->IsChildOf( + UMyActionGameplayAbility::StaticClass()); + }) + .Message(TEXT("Do not override ActivateAbility directly in UMyActionGameplayAbility.")) + .Suggest(TEXT("Move gameplay logic into OnActionStarted instead.")) + .Error() + ); + } + + virtual void BuildCallRules(TArray& OutRules) const override + { + OutRules.Add( + FORBID_CALL(UGameplayStatics::StaticClass(), TEXT("GetAllActorsOfClass")) + .Message(TEXT("GetAllActorsOfClass is not allowed in this Blueprint.")) + .Suggest(TEXT("Use a cached registry or subsystem lookup instead.")) + .PerformanceWarning() + ); + + OutRules.Add( + FORBID_CALL(UGameplayStatics::StaticClass(), TEXT("PlaySound2D")) + .When([](const UBlueprint* Blueprint) + { + return Blueprint + && Blueprint->GeneratedClass + && !Blueprint->GeneratedClass->IsChildOf(UUserWidget::StaticClass()); + }) + .Message(TEXT("PlaySound2D is only allowed from UI widgets. Gameplay Blueprints must route audio through the project audio subsystem.")) + .Suggest(TEXT("Use UGameAudioSubsystem::PlaySfx or an approved audio event wrapper instead.")) + .Error() + ); + } +*/ + + + +/** User-facing fix suggestion shown alongside a validation violation. */ +USTRUCT() +struct UE_API FEditorValidatorBlueprintSuggestion +{ + GENERATED_BODY() + + UPROPERTY() + FText DisplayText; + + TOptional CopyText; + TFunction OnExecute; + + bool bAllowCopy = false; + + FEditorValidatorBlueprintSuggestion() = default; + + explicit FEditorValidatorBlueprintSuggestion(const FText& InText) + : DisplayText(InText) + { + } + + static FEditorValidatorBlueprintSuggestion MakeCopy(const FString& Label, const FString& CopyValue) + { + FEditorValidatorBlueprintSuggestion Suggestion(FText::FromString(Label)); + Suggestion.bAllowCopy = true; + Suggestion.CopyText = CopyValue; + return Suggestion; + } + + static FEditorValidatorBlueprintSuggestion MakeAction(const FString& Label, TFunction Action) + { + FEditorValidatorBlueprintSuggestion Suggestion(FText::FromString(Label)); + Suggestion.OnExecute = MoveTemp(Action); + return Suggestion; + } +}; + +/** Generic node-level Blueprint validation rule. */ +USTRUCT() +struct UE_API FEditorValidatorBlueprintNodeRule +{ + GENERATED_BODY() + + TFunction Matches; + FText RuleText; + + TFunction&)> BuildSuggestions; + TFunction GetSeverity; +}; + +/** Function-call-specific Blueprint validation rule. */ +USTRUCT() +struct UE_API FEditorValidatorBlueprintCallRule +{ + GENERATED_BODY() + + TFunction Applies; + + UPROPERTY() + TObjectPtr OwnerRootClass = nullptr; + + FName ForbiddenFunction; + FText RuleText; + + TFunction&)> BuildSuggestions; + TFunction GetSeverity; +}; + +/** Fluent builder for node-level validation rules. */ +struct UE_API FEditorValidatorBlueprintNodeRuleBuilder +{ + FEditorValidatorBlueprintNodeRule Rule; + TArray Suggestions; + + FEditorValidatorBlueprintNodeRuleBuilder& Message(const FString& In) + { + Rule.RuleText = FText::FromString(In); + return *this; + } + + FEditorValidatorBlueprintNodeRuleBuilder& When(TFunction Condition) + { + const auto PreviousMatch = Rule.Matches; + + Rule.Matches = [PreviousMatch, Condition = MoveTemp(Condition)](UEdGraphNode* Node, const UBlueprint* Blueprint) + { + return Condition(Blueprint) && PreviousMatch && PreviousMatch(Node, Blueprint); + }; + + return *this; + } + + FEditorValidatorBlueprintNodeRuleBuilder& Suggest(const FString& In) + { + Suggestions.Add(FEditorValidatorBlueprintSuggestion(FText::FromString(In))); + return *this; + } + + FEditorValidatorBlueprintNodeRuleBuilder& SuggestCopy(const FString& Label, const FString& CopyValue) + { + Suggestions.Add(FEditorValidatorBlueprintSuggestion::MakeCopy(Label, CopyValue)); + return *this; + } + + FEditorValidatorBlueprintNodeRuleBuilder& SuggestAction(const FString& Label, TFunction Action) + { + Suggestions.Add(FEditorValidatorBlueprintSuggestion::MakeAction(Label, MoveTemp(Action))); + return *this; + } + + FEditorValidatorBlueprintNodeRuleBuilder& Info() + { + Rule.GetSeverity = [](const UBlueprint*) { return EMessageSeverity::Info; }; + return *this; + } + + FEditorValidatorBlueprintNodeRuleBuilder& PerformanceWarning() + { + Rule.GetSeverity = [](const UBlueprint*) + { + return EMessageSeverity::Warning; + }; + + const FText ExistingMessage = Rule.RuleText; + + Rule.RuleText = FText::Format( + NSLOCTEXT( + "EditorValidator", + "PerformanceWarning", + "[Performance] {0}"), + ExistingMessage + ); + + return *this; + } + + FEditorValidatorBlueprintNodeRuleBuilder& Warning() + { + Rule.GetSeverity = [](const UBlueprint*) { return EMessageSeverity::Warning; }; + return *this; + } + + FEditorValidatorBlueprintNodeRuleBuilder& Error() + { + Rule.GetSeverity = [](const UBlueprint*) { return EMessageSeverity::Error; }; + return *this; + } + + FEditorValidatorBlueprintNodeRuleBuilder& CustomSeverity(TFunction In) + { + Rule.GetSeverity = MoveTemp(In); + return *this; + } + + operator FEditorValidatorBlueprintNodeRule() + { + Rule.BuildSuggestions = + [LocalSuggestions = Suggestions](UEdGraphNode*, TArray& Out) + { + Out = LocalSuggestions; + }; + + return Rule; + } +}; + +/** Fluent builder for function-call validation rules. */ +struct UE_API FEditorValidatorBlueprintCallRuleBuilder +{ + FEditorValidatorBlueprintCallRule Rule; + TArray Suggestions; + + FEditorValidatorBlueprintCallRuleBuilder& Message(const FString& In) + { + Rule.RuleText = FText::FromString(In); + return *this; + } + + FEditorValidatorBlueprintCallRuleBuilder& When(TFunction Condition) + { + Rule.Applies = MoveTemp(Condition); + return *this; + } + + FEditorValidatorBlueprintCallRuleBuilder& Suggest(const FString& In) + { + Suggestions.Add(FEditorValidatorBlueprintSuggestion(FText::FromString(In))); + return *this; + } + + FEditorValidatorBlueprintCallRuleBuilder& SuggestCopy(const FString& Label, const FString& CopyValue) + { + Suggestions.Add(FEditorValidatorBlueprintSuggestion::MakeCopy(Label, CopyValue)); + return *this; + } + + FEditorValidatorBlueprintCallRuleBuilder& SuggestAction(const FString& Label, TFunction Action) + { + Suggestions.Add(FEditorValidatorBlueprintSuggestion::MakeAction(Label, MoveTemp(Action))); + return *this; + } + + FEditorValidatorBlueprintCallRuleBuilder& Info() + { + Rule.GetSeverity = [](const UBlueprint*) { return EMessageSeverity::Info; }; + return *this; + } + + FEditorValidatorBlueprintCallRuleBuilder& PerformanceWarning() + { + Rule.GetSeverity = [](const UBlueprint*) + { + return EMessageSeverity::Warning; + }; + + const FText ExistingMessage = Rule.RuleText; + + Rule.RuleText = FText::Format( + NSLOCTEXT( + "EditorValidator", + "PerformanceWarning", + "[Performance] {0}"), + ExistingMessage + ); + + return *this; + } + + FEditorValidatorBlueprintCallRuleBuilder& Warning() + { + Rule.GetSeverity = [](const UBlueprint*) { return EMessageSeverity::Warning; }; + return *this; + } + + FEditorValidatorBlueprintCallRuleBuilder& Error() + { + Rule.GetSeverity = [](const UBlueprint*) { return EMessageSeverity::Error; }; + return *this; + } + + FEditorValidatorBlueprintCallRuleBuilder& CustomSeverity(TFunction In) + { + Rule.GetSeverity = MoveTemp(In); + return *this; + } + + operator FEditorValidatorBlueprintCallRule() + { + Rule.BuildSuggestions = + [LocalSuggestions = Suggestions](FName, TArray& Out) + { + Out = LocalSuggestions; + }; + + return Rule; + } +}; + +/** Base validator for restricting Blueprint implementations and function calls. */ +UCLASS(Abstract) +class UE_API UEditorValidator_BlueprintRestriction : public UEditorValidatorBase +{ + GENERATED_BODY() + +protected: + virtual bool AppliesTo(UObject* Asset) const PURE_VIRTUAL(UEditorValidator_BlueprintRestriction::AppliesTo, return false;); + + virtual void BuildNodeRules(TArray& OutRules) const {} + virtual void BuildCallRules(TArray& OutRules) const {} + + FEditorValidatorBlueprintNodeRuleBuilder FORBID_OVERRIDE(const FName FunctionName) const; + FEditorValidatorBlueprintCallRuleBuilder FORBID_CALL(UClass* OwnerClass, const FName FunctionName) const; + + void ValidateBlueprint(UBlueprint* Blueprint, FDataValidationContext& Context, bool& bAnyViolation) const; + + static bool IsOverrideOf(UEdGraphNode* Node, const FName FunctionName); + static bool IsGhostEvent(const UK2Node_Event* EventNode); + + static bool TryGetCallFunctionInfo( + UEdGraphNode* Node, + UClass*& OutOwnerClass, + FName& OutNormalized, + FString& OutWhatUsed); + + static void AddViolationMessage( + FDataValidationContext& Context, + UBlueprint* Blueprint, + UEdGraph* Graph, + UEdGraphNode* Node, + EMessageSeverity::Type Severity, + const FText& RuleText, + const FString& WhatUsed, + const TArray& Suggestions); + +public: + virtual bool CanValidateAsset_Implementation( + const FAssetData& InAssetData, + UObject* InObject, + FDataValidationContext& InContext) const override; + + virtual EDataValidationResult ValidateLoadedAsset_Implementation( + const FAssetData& InAssetData, + UObject* InAsset, + FDataValidationContext& Context) override; +}; + + + +/** Developer settings for configuring Blueprint validation rules. */ +UCLASS(Config = Editor, defaultconfig, DisplayName = "Editor Blueprint Validation") +class UE_API UEditorValidatorBlueprintValidationSettings : public UDeveloperSettings +{ + GENERATED_BODY() + +public: + +}; + +/** + * Settings-backed Blueprint validator. + * + * Intentionally not exported for extension; project-specific rules should derive from + * UEditorValidator_BlueprintRestriction instead. + */ +UCLASS() +class UEditorValidator_BlueprintRestriction_SettingBased : public UEditorValidator_BlueprintRestriction +{ + GENERATED_BODY() + +protected: + virtual bool AppliesTo(UObject* Asset) const override + { + return Asset && Asset->IsA(); + } + + virtual void BuildNodeRules(TArray& OutRules) const override; +}; + +#undef UE_API \ No newline at end of file