// Copyright Epic Games, Inc. All Rights Reserved. #include "NiagaraValidationRules.h" #include "NiagaraClipboard.h" #include "NiagaraComponentRendererProperties.h" #include "NiagaraDataInterfaceCamera.h" #include "NiagaraDataInterfaceSkeletalMesh.h" #include "NiagaraDataInterfaceUtilities.h" #include "NiagaraEditorSettings.h" #include "NiagaraNodeFunctionCall.h" #include "NiagaraMeshRendererProperties.h" #include "NiagaraRibbonRendererProperties.h" #include "NiagaraScriptSource.h" #include "NiagaraSettings.h" #include "NiagaraSimulationStageBase.h" #include "NiagaraSpriteRendererProperties.h" #include "NiagaraSystemImpl.h" #include "NiagaraSystemEditorData.h" #include "DataInterface/NiagaraDataInterfaceActorComponent.h" #include "Stateless/NiagaraStatelessEmitter.h" #include "Stateless/NiagaraStatelessModule.h" #include "ViewModels/NiagaraEmitterHandleViewModel.h" #include "ViewModels/NiagaraSystemViewModel.h" #include "ViewModels/Stack/NiagaraStackEmitterSettingsGroup.h" #include "ViewModels/Stack/NiagaraStackFunctionInput.h" #include "ViewModels/Stack/NiagaraStackModuleItem.h" #include "ViewModels/Stack/NiagaraStackRendererItem.h" #include "ViewModels/Stack/NiagaraStackSystemPropertiesItem.h" #include "ViewModels/Stack/NiagaraStackViewModel.h" #include "ViewModels/NiagaraEmitterViewModel.h" #include "AssetToolsModule.h" #include "NiagaraNodeOutput.h" #include "NiagaraNodeParameterMapFor.h" #include "Materials/MaterialInterface.h" #include "ScopedTransaction.h" #include "DeviceProfiles/DeviceProfile.h" #include "DeviceProfiles/DeviceProfileManager.h" #include "ObjectEditorUtils.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(NiagaraValidationRules) #define LOCTEXT_NAMESPACE "NiagaraValidationRules" bool NiagaraValidation::HasValidationRules(UNiagaraSystem* NiagaraSystem) { if ( NiagaraSystem != nullptr ) { if (const UNiagaraEditorSettings* EditorSettings = GetDefault()) { for (const TSoftObjectPtr& ValidationRuleSetPtr : EditorSettings->DefaultValidationRuleSets) { const UNiagaraValidationRuleSet* ValidationRuleSet = ValidationRuleSetPtr.LoadSynchronous(); if (ValidationRuleSet != nullptr && ValidationRuleSet->HasAnyRules()) { return true; } } } if (UNiagaraEffectType* EffectType = NiagaraSystem->GetEffectType()) { for (UNiagaraValidationRule* Rule : EffectType->ValidationRules) { if (Rule && Rule->IsEnabled()) { return true; } } for (UNiagaraValidationRuleSet* ValidationRuleSet : EffectType->ValidationRuleSets) { if (ValidationRuleSet != nullptr && ValidationRuleSet->HasAnyRules()) { return true; } } } } return false; } // -------------------------------------------------------------------------------------------------------------------------------------------- void NiagaraValidation::ValidateAllRulesInSystem(TSharedPtr SysViewModel, TFunction ResultCallback) { if (SysViewModel == nullptr) { return; } FNiagaraValidationContext Context; Context.ViewModel = SysViewModel; TArray NiagaraValidationResults; UNiagaraSystem& NiagaraSystem = SysViewModel->GetSystem(); // Helper function const auto& ExecuteValidateRules = [&](TConstArrayView> ValidationRules) { for (const UNiagaraValidationRule* ValidationRule : ValidationRules) { if (ValidationRule && ValidationRule->IsEnabled()) { ValidationRule->CheckValidity(Context, NiagaraValidationResults); } } }; // Validate Global Rules if (const UNiagaraEditorSettings* EditorSettings = GetDefault()) { for (const TSoftObjectPtr& ValidationRuleSetPtr : EditorSettings->DefaultValidationRuleSets) { if (const UNiagaraValidationRuleSet* ValidationRuleSet = ValidationRuleSetPtr.LoadSynchronous()) { ExecuteValidateRules(ValidationRuleSet->ValidationRules); } } } // Validate EffectType Rules if (UNiagaraEffectType* EffectType = NiagaraSystem.GetEffectType()) { ExecuteValidateRules(EffectType->ValidationRules); for (UNiagaraValidationRuleSet* ValidationRuleSet : EffectType->ValidationRuleSets) { if (ValidationRuleSet != nullptr) { ExecuteValidateRules(ValidationRuleSet->ValidationRules); } } } // Validate Module Specific Rules TArray StackModuleItems = NiagaraValidation::GetAllStackEntriesInSystem(Context.ViewModel); for (UNiagaraStackModuleItem* Module : StackModuleItems) { if (Module && Module->GetIsEnabled()) { if (UNiagaraScript* Script = Module->GetModuleNode().FunctionScript) { Context.Source = Module; for (UNiagaraValidationRule* ValidationRule : Script->ValidationRules) { if (ValidationRule && ValidationRule->IsEnabled()) { ValidationRule->CheckValidity(Context, NiagaraValidationResults); } } } } } // process results for (const FNiagaraValidationResult& Result : NiagaraValidationResults) { ResultCallback(Result); } } UNiagaraStackRendererItem* NiagaraValidation::GetRendererStackItem(UNiagaraStackViewModel* StackViewModel, UNiagaraRendererProperties* RendererProperties) { TArray RendererItems = NiagaraValidation::GetStackEntries(StackViewModel); for (UNiagaraStackRendererItem* Item : RendererItems) { if (Item->GetRendererProperties() == RendererProperties) { return Item; } } return nullptr; } void NiagaraValidation::AddGoToFXTypeLink(FNiagaraValidationResult& Result, UNiagaraEffectType* FXType) { if (FXType == nullptr) { return; } FNiagaraValidationFix& GoToValidationRulesLink = Result.Links.AddDefaulted_GetRef(); GoToValidationRulesLink.Description = LOCTEXT("GoToValidationRulesFix", "Go To Validation Rules"); TWeakObjectPtr WeakFXType = FXType; GoToValidationRulesLink.FixDelegate = FNiagaraValidationFixDelegate::CreateLambda([WeakFXType] { FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked(TEXT("AssetTools")); TWeakPtr WeakAssetTypeActions = AssetToolsModule.Get().GetAssetTypeActionsForClass(UNiagaraEffectType::StaticClass()); if (UNiagaraEffectType* FXType = WeakFXType.Get()) { if (TSharedPtr AssetTypeActions = WeakAssetTypeActions.Pin()) { TArray AssetsToEdit; AssetsToEdit.Add(FXType); AssetTypeActions->OpenAssetEditor(AssetsToEdit); //TODO: Is there a way for us to auto navigate to and open up the validation rules inside FXType? } } }); } FNiagaraValidationFix NiagaraValidation::MakeDisableGPUSimulationFix(FVersionedNiagaraEmitterWeakPtr WeakEmitterPtr) { return FNiagaraValidationFix( LOCTEXT("GpuUsageInfoFix_SwitchToCput", "Set emitter to CPU"), FNiagaraValidationFixDelegate::CreateLambda( [WeakEmitterPtr]() { FVersionedNiagaraEmitter VersionedEmitter = WeakEmitterPtr.ResolveWeakPtr(); if (FVersionedNiagaraEmitterData* VersionedEmitterData = VersionedEmitter.GetEmitterData()) { const FScopedTransaction Transaction(LOCTEXT("SetCPUSim", "Set CPU Simulation")); VersionedEmitter.Emitter->Modify(); VersionedEmitterData->SimTarget = ENiagaraSimTarget::CPUSim; FProperty* SimTargetProperty = FindFProperty(FVersionedNiagaraEmitterData::StaticStruct(), GET_MEMBER_NAME_CHECKED(FVersionedNiagaraEmitterData, SimTarget)); FPropertyChangedEvent PropertyChangedEvent(SimTargetProperty); VersionedEmitter.Emitter->PostEditChangeVersionedProperty(PropertyChangedEvent, VersionedEmitter.Version); UNiagaraSystem::RequestCompileForEmitter(VersionedEmitter); } } ) ); } TArray NiagaraValidation::GatherPlatformSetConflicts(const FNiagaraPlatformSet* SetA, const FNiagaraPlatformSet* SetB) { TArray PlatformSets = {SetA, SetB}; TArray Conflicts; FNiagaraPlatformSet::GatherConflicts(PlatformSets, Conflicts); return MoveTemp(Conflicts); } FString NiagaraValidation::GetPlatformConflictsString(TConstArrayView ConflictInfos, int MaxPlatformsToShow) { if (ConflictInfos.Num() > 0) { TSet ConflictPlatformNames; for (const FNiagaraPlatformSetConflictInfo& ConflictInfo : ConflictInfos) { for (const FNiagaraPlatformSetConflictEntry& ConflictEntry : ConflictInfo.Conflicts) { ConflictPlatformNames.Add(ConflictEntry.ProfileName); } } TStringBuilder<256> ConflictPlatformsString; int NumFounds = 0; for (FName PlatformName : ConflictPlatformNames) { if (NumFounds >= MaxPlatformsToShow) { ConflictPlatformsString.Append(TEXT(", ...")); break; } if (NumFounds != 0) { ConflictPlatformsString.Append(TEXT(", ")); } ++NumFounds; PlatformName.AppendString(ConflictPlatformsString); } return ConflictPlatformsString.ToString(); } return FString(); } FString NiagaraValidation::GetPlatformConflictsString(const FNiagaraPlatformSet& PlatformSetA, const FNiagaraPlatformSet& PlatformSetB, int MaxPlatformsToShow) { TArray> CheckSets; CheckSets.Add(&PlatformSetA); CheckSets.Add(&PlatformSetB); TArray ConflictInfos; FNiagaraPlatformSet::GatherConflicts(CheckSets, ConflictInfos); return GetPlatformConflictsString(ConflictInfos, MaxPlatformsToShow); } TSharedPtr NiagaraValidation::GetEmitterViewModel(const FNiagaraValidationContext& Context, UNiagaraEmitter* NiagaraEmitter) { if (NiagaraEmitter == nullptr) { return nullptr; } const TSharedRef* EmitterViewModel = Context.ViewModel->GetEmitterHandleViewModels().FindByPredicate( [NiagaraEmitter](const TSharedRef& EmitterViewModelRef) { FNiagaraEmitterHandle* EmitterHandle = EmitterViewModelRef->GetEmitterHandle(); return EmitterHandle && EmitterHandle->GetInstance().Emitter == NiagaraEmitter; } ); if ( EmitterViewModel ) { return *EmitterViewModel; } return nullptr; } TOptional NiagaraValidation::GetModuleStaticInt32Value(const UNiagaraStackModuleItem* Module, FName ParameterName) { TArray ModuleInputs; Module->GetParameterInputs(ModuleInputs); for (UNiagaraStackFunctionInput* Input : ModuleInputs) { if (Input->IsStaticParameter() && Input->GetInputParameterHandle().GetName() == ParameterName) { return TOptional(*(int32*)Input->GetLocalValueStruct()->GetStructMemory()); } } return TOptional(); } void NiagaraValidation::SetModuleStaticInt32Value(UNiagaraStackModuleItem* Module, FName ParameterName, int32 NewValue) { TArray ModuleInputs; Module->GetParameterInputs(ModuleInputs); for (UNiagaraStackFunctionInput* Input : ModuleInputs) { if (Input->IsStaticParameter() && Input->GetInputParameterHandle().GetName() == ParameterName) { TSharedRef ValueStruct = MakeShared(Input->GetLocalValueStruct()->GetStruct()); *(int32*)ValueStruct->GetStructMemory() = NewValue; Input->SetLocalValue(ValueStruct); } } } bool NiagaraValidation::StructContainsUObjectProperty(UStruct* Struct) { for (TFieldIterator PropertyIt(Struct); PropertyIt; ++PropertyIt) { const FProperty* Property = *PropertyIt; if (const FArrayProperty* ArrayProperty = CastField(Property)) { // If we are an array change the property to be the inner one to check for struct / object Property = ArrayProperty->Inner; } if (const FStructProperty* StructProperty = CastField(Property)) { if (StructProperty->Struct) { if (StructContainsUObjectProperty(StructProperty->Struct)) { return true; } } } else if (CastField(Property) || CastField(Property) || CastField(Property)) { return true; } } return false; } void UNiagaraValidationRule_NoWarmupTime::CheckValidity(const FNiagaraValidationContext& Context, TArray& Results) const { UNiagaraSystem& System = Context.ViewModel->GetSystem(); if (System.NeedsWarmup()) { UNiagaraStackSystemPropertiesItem* SystemProperties = NiagaraValidation::GetStackEntry(Context.ViewModel->GetSystemStackViewModel()); FNiagaraValidationResult Result(ENiagaraValidationSeverity::Error, LOCTEXT("WarumupSummary", "Warmuptime > 0 is not allowed"), LOCTEXT("WarmupDescription", "Systems with the chosen effect type do not allow warmup time, as it costs too much performance.\nPlease set the warmup time to 0 in the system properties."), SystemProperties); Results.Add(Result); } } void UNiagaraValidationRule_NoEvents::CheckValidity(const FNiagaraValidationContext& Context, TArray& OutResults) const { TArray> EmitterHandleViewModels = Context.ViewModel->GetEmitterHandleViewModels(); for (TSharedRef EmitterHandleModel : EmitterHandleViewModels) { FNiagaraEmitterHandle* EmitterHandle = EmitterHandleModel.Get().GetEmitterHandle(); if (!EmitterHandle->GetIsEnabled()) { continue; } FVersionedNiagaraEmitterData* EmitterData = EmitterHandleModel.Get().GetEmitterHandle()->GetEmitterData(); if (!EmitterData || EmitterData->GetEventHandlers().Num() == 0) { continue; } TArray Conflicts = NiagaraValidation::GatherPlatformSetConflicts(&Platforms, &EmitterData->Platforms); if (Conflicts.Num() == 0) { continue; } const FString PlatformConflicts = NiagaraValidation::GetPlatformConflictsString(Conflicts); FNiagaraValidationResult& Result = OutResults.AddDefaulted_GetRef(); Result.Severity = Severity; Result.SummaryText = LOCTEXT("NoEventsSummary", "Events are not allowed."); Result.Description = FText::Format(LOCTEXT("NoEventsDesc", "Events are not allowed on '{0}'."), FText::FromString(PlatformConflicts)); Result.SourceObject = NiagaraValidation::GetStackEntry(EmitterHandleModel.Get().GetEmitterStackViewModel()); } } void UNiagaraValidationRule_FixedGPUBoundsSet::CheckValidity(const FNiagaraValidationContext& Context, TArray& Results) const { // if the system has fixed bounds set then it overrides the emitter settings if (Context.ViewModel->GetSystem().bFixedBounds) { return; } // check that all the gpu emitters have fixed bounds set TArray> EmitterHandleViewModels = Context.ViewModel->GetEmitterHandleViewModels(); for (TSharedRef EmitterHandleModel : EmitterHandleViewModels) { if (EmitterHandleModel->GetIsEnabled() == false) { continue; } FVersionedNiagaraEmitterData* EmitterData = EmitterHandleModel.Get().GetEmitterHandle()->GetEmitterData(); if (EmitterData && EmitterData->SimTarget == ENiagaraSimTarget::GPUComputeSim && EmitterData->CalculateBoundsMode == ENiagaraEmitterCalculateBoundMode::Dynamic) { UNiagaraStackEmitterPropertiesItem* EmitterProperties = NiagaraValidation::GetStackEntry(EmitterHandleModel.Get().GetEmitterStackViewModel()); FNiagaraValidationResult Result(ENiagaraValidationSeverity::Error, LOCTEXT("GpuDynamicBoundsErrorSummary", "GPU emitters do not support dynamic bounds"), LOCTEXT("GpuDynamicBoundsErrorDescription", "Gpu emitter should either not be in dynamic mode or the system must have fixed bounds."), EmitterProperties); Results.Add(Result); } } } bool IsEnabledForMaxQualityLevel(FNiagaraPlatformSet Platforms, int32 MaxQualityLevel) { for (int i = 0; i < MaxQualityLevel; i++) { if (Platforms.IsEnabledForQualityLevel(i)) { return true; } } return false; } void UNiagaraValidationRule_EmitterCount::CheckValidity(const FNiagaraValidationContext& Context, TArray& OutResults) const { const int32 NumEmitterCountLimits = EmitterCountLimits.Num(); if (NumEmitterCountLimits == 0) { return; } TArray, TInlineAllocator<8>> ConflictsPerLimit; TArray> EmitterCountPerLimit; EmitterCountPerLimit.AddDefaulted(NumEmitterCountLimits); ConflictsPerLimit.AddDefaulted(NumEmitterCountLimits); UNiagaraSystem& System = Context.ViewModel->GetSystem(); TArray> EmitterHandleViewModels = Context.ViewModel->GetEmitterHandleViewModels(); for (TSharedRef EmitterHandleModel : EmitterHandleViewModels) { FNiagaraEmitterHandle* EmitterHandle = EmitterHandleModel.Get().GetEmitterHandle(); if (!EmitterHandle->GetIsEnabled()) { continue; } const FNiagaraPlatformSet* EmitterPlatformSet = EmitterHandle->GetPlatformSet(); if (!EmitterPlatformSet) { continue; } const bool bIsStateful = EmitterHandle->GetEmitterMode() == ENiagaraEmitterMode::Standard; const bool bIsStateless = EmitterHandle->GetEmitterMode() == ENiagaraEmitterMode::Stateless; for (int32 i=0; i < NumEmitterCountLimits; ++i) { if ((EmitterCountLimits[i].bIncludeStateful && bIsStateful) || (EmitterCountLimits[i].bIncludeStateless && bIsStateless)) { const TArray Conflicts = NiagaraValidation::GatherPlatformSetConflicts(&EmitterCountLimits[i].Platforms, EmitterPlatformSet); if (Conflicts.Num() > 0) { ConflictsPerLimit[i].Append(Conflicts); ++EmitterCountPerLimit[i]; } } } } for (int32 i=0; i < NumEmitterCountLimits; ++i) { const int32 EmitterCountLimit = EmitterCountLimits[i].EmitterCountLimit; if (EmitterCountPerLimit[i] <= EmitterCountLimit) { continue; } const FString PlatformConflicts = NiagaraValidation::GetPlatformConflictsString(ConflictsPerLimit[i]); FText RuleName; if (EmitterCountLimits[i].RuleName.IsEmpty()) { RuleName = LOCTEXT("EmitterCountLimitExceeded", "Emitter count limit exceeded"); } else { RuleName = FText::Format(LOCTEXT("EmitterCountLimitExceededFmt", "Emitter count limit '{0}' exceeded"), FText::FromString(EmitterCountLimits[i].RuleName)); } FNiagaraValidationResult& Result = OutResults.AddDefaulted_GetRef(); Result.Severity = Severity; Result.SummaryText = FText::Format(LOCTEXT("EmitterCountLimit", "{0} {1}/{2}."), RuleName, EmitterCountPerLimit[i], EmitterCountLimit); Result.Description = FText::Format(LOCTEXT("EmitterCountLimitDesc", "{0} {1}/{2} for platforms '{3}' please reduce the emitter count to improve performance."), RuleName, EmitterCountPerLimit[i], EmitterCountLimit, FText::FromString(PlatformConflicts)); Result.SourceObject = NiagaraValidation::GetStackEntry(Context.ViewModel->GetSystemStackViewModel()); } } void UNiagaraValidationRule_RendererCount::CheckValidity(const FNiagaraValidationContext& Context, TArray& OutResults) const { const int32 NumRendererCountLimits = RendererCountLimits.Num(); if (NumRendererCountLimits == 0) { return; } TArray, TInlineAllocator<8>> ConflictsPerLimit; TArray> RendererCountPerLimit; RendererCountPerLimit.AddDefaulted(NumRendererCountLimits); ConflictsPerLimit.AddDefaulted(NumRendererCountLimits); UNiagaraSystem& System = Context.ViewModel->GetSystem(); TArray> EmitterHandleViewModels = Context.ViewModel->GetEmitterHandleViewModels(); for (TSharedRef EmitterHandleModel : EmitterHandleViewModels) { FNiagaraEmitterHandle* EmitterHandle = EmitterHandleModel.Get().GetEmitterHandle(); if (!EmitterHandle->GetIsEnabled()) { continue; } FVersionedNiagaraEmitterData* EmitterData = EmitterHandleModel.Get().GetEmitterHandle()->GetEmitterData(); if (EmitterData == nullptr) { continue; } for (int32 i=0; i < NumRendererCountLimits; ++i) { if ( NiagaraValidation::GatherPlatformSetConflicts(&RendererCountLimits[i].Platforms, &EmitterData->Platforms).Num() == 0 ) { continue; } EmitterData->ForEachRenderer( [this, i, &ConflictsPerLimit, &RendererCountPerLimit, &EmitterData](UNiagaraRendererProperties* RendererProperties) { if (RendererProperties->GetIsEnabled()) { TArray Conflicts = NiagaraValidation::GatherPlatformSetConflicts(&RendererCountLimits[i].Platforms, &EmitterData->Platforms); if (Conflicts.Num() > 0) { ConflictsPerLimit[i].Append(Conflicts); ++RendererCountPerLimit[i]; } } } ); } } for (int32 i = 0; i < NumRendererCountLimits; ++i) { const int32 RendererCountLimit = RendererCountLimits[i].RendererCountLimit; if (RendererCountPerLimit[i] <= RendererCountLimit) { continue; } const FString PlatformConflicts = NiagaraValidation::GetPlatformConflictsString(ConflictsPerLimit[i]); FText RuleName; if (RendererCountLimits[i].RuleName.IsEmpty()) { RuleName = LOCTEXT("RendererCountLimitExceeded", "Renderer count limit exceeded"); } else { RuleName = FText::Format(LOCTEXT("RendererCountLimitExceededFmt", "Renderer count limit '{0}' exceeded"), FText::FromString(RendererCountLimits[i].RuleName)); } FNiagaraValidationResult& Result = OutResults.AddDefaulted_GetRef(); Result.Severity = Severity; Result.SummaryText = FText::Format(LOCTEXT("RendererCountLimit", "{0} {1}/{2}."), RuleName, RendererCountPerLimit[i], RendererCountLimit); Result.Description = FText::Format(LOCTEXT("RendererCountLimitDesc", "{0} {1}/{2} for platforms '{3}' please reduce the renderer count to improve performance."), RuleName, RendererCountPerLimit[i], RendererCountLimit, FText::FromString(PlatformConflicts)); Result.SourceObject = NiagaraValidation::GetStackEntry(Context.ViewModel->GetSystemStackViewModel()); } } void UNiagaraValidationRule_BannedRenderers::CheckValidity(const FNiagaraValidationContext& Context, TArray& Results) const { UNiagaraSystem& System = Context.ViewModel->GetSystem(); TArray> EmitterHandleViewModels = Context.ViewModel->GetEmitterHandleViewModels(); for (TSharedRef EmitterHandleModel : EmitterHandleViewModels) { if ( EmitterHandleModel->GetIsEnabled() == false) { continue; } FVersionedNiagaraEmitterData* EmitterData = EmitterHandleModel.Get().GetEmitterHandle()->GetEmitterData(); if (EmitterData == nullptr) { continue; } EmitterData->ForEachRenderer([&Results, EmitterHandleModel, this, &System](UNiagaraRendererProperties* RendererProperties) { if (RendererProperties->GetIsEnabled() && BannedRenderers.Contains(RendererProperties->GetClass())) { TArray Conflicts = NiagaraValidation::GatherPlatformSetConflicts(&Platforms, &RendererProperties->Platforms); if (Conflicts.Num() > 0) { if ( UNiagaraStackRendererItem* StackItem = NiagaraValidation::GetRendererStackItem(EmitterHandleModel.Get().GetEmitterStackViewModel(), RendererProperties) ) { FNiagaraValidationResult& Result = Results.AddDefaulted_GetRef(); Result.Severity = Severity; Result.SummaryText = LOCTEXT("BannedRenderSummary", "Banned renderers used."); Result.Description = LOCTEXT("BannedRenderDescription", "Please ensure only allowed renderers are used for each platform according to the validation rules in the System's Effect Type."); Result.SourceObject = StackItem; NiagaraValidation::AddGoToFXTypeLink(Result, System.GetEffectType()); //Add autofix to disable the module FNiagaraValidationFix& DisableRendererFix = Result.Fixes.AddDefaulted_GetRef(); DisableRendererFix.Description = LOCTEXT("DisableBannedRendererFix", "Disable Banned Renderer"); TWeakObjectPtr WeakRendererItem = StackItem; DisableRendererFix.FixDelegate = FNiagaraValidationFixDelegate::CreateLambda( [WeakRendererItem]() { if (UNiagaraStackRendererItem* RendererItem = WeakRendererItem.Get()) { RendererItem->SetIsEnabled(false); } } ); } } } }); } } void UNiagaraValidationRule_Lightweight::CheckValidity(const FNiagaraValidationContext& Context, TArray& OutResults) const { UNiagaraSystem& System = Context.ViewModel->GetSystem(); TArray> EmitterHandleViewModels = Context.ViewModel->GetEmitterHandleViewModels(); for (TSharedRef EmitterHandleModel : EmitterHandleViewModels) { FNiagaraEmitterHandle* EmitterHandle = EmitterHandleModel->GetEmitterHandle(); UNiagaraStatelessEmitter* StatelessEmitter = EmitterHandle && EmitterHandle->GetEmitterMode() == ENiagaraEmitterMode::Stateless ? EmitterHandle->GetStatelessEmitter() : nullptr; if (StatelessEmitter == nullptr) { continue; } TArray Conflicts = NiagaraValidation::GatherPlatformSetConflicts(&Platforms, &StatelessEmitter->GetPlatformSet()); if (Conflicts.Num() == 0) { continue; } if (UsedWithEmitter.IsSet()) { OutResults.Emplace( UsedWithEmitter.GetValue(), LOCTEXT("StatelessNotAllowed", "Lightweight emitter is being used."), FText::Format(LOCTEXT("StatelessNotAllowedDesc", "Lightweight emitter {0} is not allowed, please disable or remove."), FText::FromName(StatelessEmitter->GetFName())), NiagaraValidation::GetStackEntry(Context.ViewModel->GetSystemStackViewModel()) ); } if (UsingExperimentalModule.IsSet()) { for (const UNiagaraStatelessModule* StatelessModule : StatelessEmitter->GetModules()) { if (!StatelessModule || !StatelessModule->IsModuleEnabled()) { continue; } bool bIsExperimental = false; bool bIsEarlyAccess = false; FString MostDerivedDevelopmentClassName; FObjectEditorUtils::GetClassDevelopmentStatus(StatelessModule->GetClass(), bIsExperimental, bIsEarlyAccess, MostDerivedDevelopmentClassName); if (bIsExperimental || bIsEarlyAccess) { OutResults.Emplace( UsingExperimentalModule.GetValue(), LOCTEXT("StatelessModuleNotAllowed", "Experimental lightweight modules are being used."), FText::Format(LOCTEXT("StatelessModuleNotAllowedDesc", "Experimental lightweight module {0} is not allowed, please disable or remove."), StatelessModule->GetClass()->GetDisplayNameText()), NiagaraValidation::GetStackEntry(Context.ViewModel->GetSystemStackViewModel()) ); } } } } } void UNiagaraValidationRule_BannedModules::CheckValidity(const FNiagaraValidationContext& Context, TArray& Results) const { UNiagaraSystem& System = Context.ViewModel->GetSystem(); TArray StackModuleItems = NiagaraValidation::GetAllStackEntriesInSystem(Context.ViewModel); for (UNiagaraStackModuleItem* Item : StackModuleItems) { if (!Item || !Item->GetIsEnabled()) { continue; } UNiagaraNodeFunctionCall& FuncCall = Item->GetModuleNode(); for (UNiagaraScript* BannedModule : BannedModules) { if (BannedModule != FuncCall.FunctionScript) { continue; } FVersionedNiagaraEmitterData* EmitterData = Item->GetEmitterViewModel().IsValid() ? Item->GetEmitterViewModel()->GetEmitter().GetEmitterData() : nullptr; if ( EmitterData ) { bool bApplyBan = (bBanOnCpu && EmitterData->SimTarget == ENiagaraSimTarget::CPUSim) || (bBanOnGpu && EmitterData->SimTarget == ENiagaraSimTarget::GPUComputeSim); //If we're on an emitter, this emitter may be culled on the platforms the rule applies to. const TArray Conflicts = NiagaraValidation::GatherPlatformSetConflicts(&Platforms, &EmitterData->Platforms); bApplyBan &= Conflicts.Num() > 0; if (!bApplyBan) { continue; } } else if (!bBanOnCpu) { // System & Emitter scripts only run on the CPU continue; } const FTextFormat Format(LOCTEXT("BannedModuleFormat", "Module {0} is banned on some currently enabled platforms")); const FText WarningMessage = FText::Format(Format, FText::FromString(FuncCall.FunctionScript->GetName())); FNiagaraValidationResult& Result = Results.AddDefaulted_GetRef(); Result.Severity = Severity; Result.SummaryText = WarningMessage; Result.Description = LOCTEXT("BanndeModulesDescription", "Check this module against the Effect Type's Banned Modules validators"); Result.SourceObject = Item; NiagaraValidation::AddGoToFXTypeLink(Result, System.GetEffectType()); //Add autofix to disable the module FNiagaraValidationFix& DisableModuleFix = Result.Fixes.AddDefaulted_GetRef(); DisableModuleFix.Description = LOCTEXT("DisableBannedModuleFix", "Disable Banned Module"); TWeakObjectPtr WeakModuleItem = Item; DisableModuleFix.FixDelegate = FNiagaraValidationFixDelegate::CreateLambda([WeakModuleItem]() { if (UNiagaraStackModuleItem* ModuleItem = WeakModuleItem.Get()) { ModuleItem->SetEnabled(false); } }); } } } void UNiagaraValidationRule_BannedDataInterfaces::CheckValidity(const FNiagaraValidationContext& Context, TArray& Results) const { UNiagaraSystem* NiagaraSystem = &Context.ViewModel->GetSystem(); FNiagaraDataInterfaceUtilities::ForEachDataInterface( NiagaraSystem, [&](const FNiagaraDataInterfaceUtilities::FDataInterfaceUsageContext& UsageContext) -> bool { UClass* DIClass = UsageContext.DataInterface->GetClass(); if (BannedDataInterfaces.Contains(DIClass) == false) { return true; } static const FText WarningFormat(LOCTEXT("BannedDataInteraceFormatWarn", "DataInterface '{0}' is banned on currently enabled platforms")); static const FText SystemDescFormat(LOCTEXT("BannedDataInteraceSystemFormatDesc", "DataInterface '{0} - {1}' is banned on currently enabled platforms")); static const FText EmitterDescFormat(LOCTEXT("BannedDataInteraceEmitterFormatDesc", "DataInterface '{0} - {1}' is banned on currently enabled platforms '{2}'")); UObject* WarningObject = nullptr; if (UNiagaraEmitter* NiagaraEmitter = Cast(const_cast(UsageContext.OwnerObject))) { const TSharedPtr EmitterViewModel = NiagaraValidation::GetEmitterViewModel(Context, NiagaraEmitter); if (EmitterViewModel == nullptr) { return true; } const FVersionedNiagaraEmitterData* EmitterData = EmitterViewModel->GetEmitterHandle()->GetEmitterData(); if (EmitterData == nullptr) { return true; } const bool bIsBanEnabled = ((EmitterData->SimTarget == ENiagaraSimTarget::CPUSim) && bBanOnCpu) || ((EmitterData->SimTarget == ENiagaraSimTarget::GPUComputeSim) && bBanOnGpu); if (bIsBanEnabled) { FString PlatformConflictsString = NiagaraValidation::GetPlatformConflictsString(Platforms, EmitterData->Platforms); if (PlatformConflictsString.IsEmpty() == false) { Results.Emplace( Severity, FText::Format(WarningFormat, FText::FromName(UsageContext.Variable.GetName())), FText::Format(EmitterDescFormat, FText::FromName(UsageContext.Variable.GetName()), FText::FromName(DIClass->GetFName()), FText::FromString(PlatformConflictsString)), NiagaraValidation::GetStackEntry(EmitterViewModel->GetEmitterStackViewModel()) ); } } } else if (const UNiagaraSystem* NiagaraSystem = Cast(UsageContext.OwnerObject)) { if (bBanOnCpu == true) { Results.Emplace( Severity, FText::Format(WarningFormat, FText::FromName(UsageContext.Variable.GetName())), FText::Format(SystemDescFormat, FText::FromName(UsageContext.Variable.GetName()), FText::FromName(DIClass->GetFName())), NiagaraValidation::GetStackEntry(Context.ViewModel->GetSystemStackViewModel()) ); } } return true; } ); } template FNiagaraValidationResult* NiagaraRendererCheckSortingEnabled(const TSharedRef& EmitterHandleModel, UNiagaraRendererProperties* InProperties, TArray& Results, ENiagaraValidationSeverity Severity) { TRendererType* Properties = Cast(InProperties); if (!Properties || !Properties->GetIsEnabled() || Properties->SortMode == ENiagaraSortMode::None) { return nullptr; } UNiagaraStackRendererItem* StackItem = NiagaraValidation::GetRendererStackItem(EmitterHandleModel.Get().GetEmitterStackViewModel(), Properties); if (StackItem == nullptr) { return nullptr; } FNiagaraValidationResult& Result = Results.AddDefaulted_GetRef(); Result.SummaryText = LOCTEXT("RendererSortingEnabled", "Sorting is enabled on the renderer."); Result.Description = LOCTEXT("RendererSortingEnabledDesc", "Sorting is enabled on the renderer, this costs performance consider if it can be disabled or not."); Result.Severity = Severity; Result.SourceObject = StackItem; Result.Fixes.Emplace( LOCTEXT("DisableSortingFix", "Disable sorting on the renderer"), FNiagaraValidationFixDelegate::CreateLambda( [WeakRenderer=MakeWeakObjectPtr(Properties)]() { if (WeakRenderer.IsValid()) { const FScopedTransaction Transaction(LOCTEXT("DisableSorting", "Disable Sorting")); WeakRenderer.Get()->Modify(); WeakRenderer.Get()->SortMode = ENiagaraSortMode::None; } } ) ); return &Result; } void UNiagaraValidationRule_RendererSortingEnabled::CheckValidity(const FNiagaraValidationContext& Context, TArray& Results) const { UNiagaraSystem& System = Context.ViewModel->GetSystem(); TArray> EmitterHandleViewModels = Context.ViewModel->GetEmitterHandleViewModels(); for (TSharedRef EmitterHandleModel : EmitterHandleViewModels) { if (EmitterHandleModel->GetIsEnabled() == false) { continue; } FVersionedNiagaraEmitterData* EmitterData = EmitterHandleModel.Get().GetEmitterHandle()->GetEmitterData(); if (!EmitterData) { continue; } const FString PlatformConflictsString = NiagaraValidation::GetPlatformConflictsString(Platforms, EmitterData->Platforms); if (PlatformConflictsString.IsEmpty()) { continue; } EmitterData->ForEachRenderer( [&Results, EmitterHandleModel, this, &System](UNiagaraRendererProperties* RendererProperties) { if ( NiagaraRendererCheckSortingEnabled(EmitterHandleModel, RendererProperties, Results, Severity) ) { return; } if ( NiagaraRendererCheckSortingEnabled(EmitterHandleModel, RendererProperties, Results, Severity) ) { return; } } ); } } void UNiagaraValidationRule_GpuUsage::CheckValidity(const FNiagaraValidationContext& Context, TArray& OutResults) const { UNiagaraSystem& System = Context.ViewModel->GetSystem(); for (TSharedRef EmitterHandleModel : Context.ViewModel->GetEmitterHandleViewModels()) { FNiagaraEmitterHandle* EmitterHandle = EmitterHandleModel.Get().GetEmitterHandle(); if (!EmitterHandle || EmitterHandle->GetIsEnabled() == false) { continue; } FVersionedNiagaraEmitterData* EmitterData = EmitterHandle->GetEmitterData(); if (!EmitterData || EmitterData->SimTarget != ENiagaraSimTarget::GPUComputeSim) { continue; } const FString PlatformConflictsString = NiagaraValidation::GetPlatformConflictsString(Platforms, EmitterData->Platforms); if (PlatformConflictsString.IsEmpty()) { continue; } FNiagaraValidationResult& ValidationResult = OutResults.Emplace_GetRef( Severity, LOCTEXT("GpuUsageInfo", "GPU usage may not function as expected"), FText::Format(LOCTEXT("GpuUsageInfoDetails", "GPU usage may not function as expected on '{0}'."), FText::FromString(PlatformConflictsString)), NiagaraValidation::GetStackEntry(EmitterHandleModel->GetEmitterStackViewModel()) ); ValidationResult.Fixes.Emplace( FText::Format(LOCTEXT("GpuUsageInfoFix_DisablePlatforms", "Disable emitter on '{0}'."), FText::FromString(PlatformConflictsString)), FNiagaraValidationFixDelegate::CreateLambda( [WeakEmitterPtr=EmitterHandle->GetInstance().ToWeakPtr(), PlatformsToDisable=Platforms]() { FVersionedNiagaraEmitter VersionedEmitter = WeakEmitterPtr.ResolveWeakPtr(); if (FVersionedNiagaraEmitterData* VersionedEmitterData = VersionedEmitter.GetEmitterData()) { TArray ConflictInfos; FNiagaraPlatformSet::GatherConflicts({&VersionedEmitterData->Platforms, &PlatformsToDisable}, ConflictInfos); for (const FNiagaraPlatformSetConflictInfo& ConflictInfo : ConflictInfos) { for (const FNiagaraPlatformSetConflictEntry& ConflictEntry : ConflictInfo.Conflicts) { UDeviceProfile* DeviceProfile = UDeviceProfileManager::Get().FindProfile(ConflictEntry.ProfileName.ToString()); for (int32 iQualityLevel=0; iQualityLevel < 32; ++iQualityLevel) { if ( (ConflictEntry.QualityLevelMask & (1 << iQualityLevel)) != 0 ) { VersionedEmitterData->Platforms.SetDeviceProfileState(DeviceProfile, iQualityLevel, ENiagaraPlatformSelectionState::Disabled); } } } } } } ) ); ValidationResult.Fixes.Emplace( NiagaraValidation::MakeDisableGPUSimulationFix(EmitterHandle->GetInstance().ToWeakPtr()) ); } } void UNiagaraValidationRule_RibbonRenderer::CheckValidity(const FNiagaraValidationContext& Context, TArray& Results) const { UNiagaraSystem& System = Context.ViewModel->GetSystem(); for (TSharedRef EmitterHandleModel : Context.ViewModel->GetEmitterHandleViewModels()) { if (EmitterHandleModel->GetIsEnabled() == false) { continue; } FNiagaraEmitterHandle* EmitterHandle = EmitterHandleModel.Get().GetEmitterHandle(); FVersionedNiagaraEmitterData* EmitterData = EmitterHandle->GetEmitterData(); if (EmitterData == nullptr) { continue; } if (NiagaraValidation::GetPlatformConflictsString(Platforms, EmitterData->Platforms).IsEmpty()) { continue; } EmitterData->ForEachRenderer( [this, &Context, &Results, &EmitterData, &EmitterHandleModel, EmitterHandle](UNiagaraRendererProperties* RendererProperties) { UNiagaraRibbonRendererProperties* RibbonRenderer = Cast(RendererProperties); UNiagaraStackRendererItem* StackItem = NiagaraValidation::GetRendererStackItem(EmitterHandleModel.Get().GetEmitterStackViewModel(), RendererProperties); if (!RibbonRenderer || !StackItem) { return; } const FString PlatformConflictsString = NiagaraValidation::GetPlatformConflictsString(Platforms, RendererProperties->Platforms); if (PlatformConflictsString.IsEmpty()) { return; } if (bFailIfUsedByGPUSimulation && EmitterData->SimTarget == ENiagaraSimTarget::GPUComputeSim) { FNiagaraValidationResult& ValidationResult = Results.Emplace_GetRef( Severity, LOCTEXT("RibbonRenderer_GpuSimulationError", "Ribbon Renderer is used with GPU simulation"), FText::Format(LOCTEXT("RibbonRenderer_GpuSimulationErrorDetails", "Ribbon Renderer is used with GPU simulation and may not function as expected on '{0}'."), FText::FromString(PlatformConflictsString)), StackItem ); ValidationResult.Fixes.Emplace( NiagaraValidation::MakeDisableGPUSimulationFix(EmitterHandle->GetInstance().ToWeakPtr()) ); } if (bFailIfUsedByGPUInit && EmitterData->SimTarget != ENiagaraSimTarget::GPUComputeSim && RibbonRenderer->bUseGPUInit) { FNiagaraValidationResult& ValidationResult = Results.Emplace_GetRef( Severity, LOCTEXT("RibbonRenderer_GpuInitError", "Ribbon Renderer is used with GPU init"), FText::Format(LOCTEXT("RibbonRenderer_GpuInitErrorDetails", "Ribbon Renderer is used with GPU init and may not function as expected on '{0}'."), FText::FromString(PlatformConflictsString)), StackItem ); ValidationResult.Fixes.Emplace( LOCTEXT("RibbonRenderer_GpuInitErrorFix", "Disable GPU init"), FNiagaraValidationFixDelegate::CreateLambda( [WeakRibbonRenderer=MakeWeakObjectPtr(RibbonRenderer)]() { if (WeakRibbonRenderer.IsValid()) { const FScopedTransaction Transaction(LOCTEXT("RibbonRenderer_GpuInitErrorApplyFix", "Disable GPU Init")); WeakRibbonRenderer.Get()->Modify(); WeakRibbonRenderer.Get()->bUseGPUInit = false; FProperty* Property = FindFProperty(UNiagaraRibbonRendererProperties::StaticClass(), GET_MEMBER_NAME_CHECKED(UNiagaraRibbonRendererProperties, bUseGPUInit)); FPropertyChangedEvent PropertyChangedEvent(Property); WeakRibbonRenderer.Get()->PostEditChangeProperty(PropertyChangedEvent); } } ) ); } } ); } } void UNiagaraValidationRule_InvalidEffectType::CheckValidity(const FNiagaraValidationContext& Context, TArray& Results) const { UNiagaraStackSystemPropertiesItem* SystemProperties = NiagaraValidation::GetStackEntry(Context.ViewModel->GetSystemStackViewModel()); FNiagaraValidationResult Result(ENiagaraValidationSeverity::Error, LOCTEXT("InvalidEffectSummary", "Invalid Effect Type"), LOCTEXT("InvalidEffectDescription", "The effect type on this system was marked as invalid for production content and should only be used as placeholder."), SystemProperties); Results.Add(Result); } void UNiagaraValidationRule_HasEffectType::CheckValidity(const FNiagaraValidationContext& Context, TArray& OutResults) const { const UNiagaraSettings* Settings = GetDefault(); UNiagaraSystem* System = &Context.ViewModel->GetSystem(); if(System->GetEffectType() == nullptr) { UNiagaraStackSystemPropertiesItem* SystemProperties = NiagaraValidation::GetStackEntry(Context.ViewModel->GetSystemStackViewModel()); FNiagaraValidationResult Result( Severity, LOCTEXT("SystemNotUsingEffectTypeIssue", "No Effect Type Specified"), LOCTEXT("SystemNotUsingEffectTypeIssueLong", "This system does not have an Effect Type assigned."), SystemProperties); if(Settings->GetDefaultEffectType() != nullptr) { TWeakObjectPtr SystemWeak = System; TWeakObjectPtr DefaultEffectTypeWeak = Settings->GetDefaultEffectType(); FNiagaraValidationFix& Fix = Result.Fixes.AddDefaulted_GetRef(); Fix.Description = LOCTEXT("SwitchToDefaultEffectType", "Switch to the default effect type for this project."); Fix.FixDelegate = FNiagaraValidationFixDelegate::CreateLambda([SystemWeak, DefaultEffectTypeWeak]() { if (SystemWeak.IsValid() && DefaultEffectTypeWeak.IsValid()) { SystemWeak->SetEffectType(DefaultEffectTypeWeak.Get()); } }); } OutResults.Add(Result); } } void UDEPRECATED_NiagaraValidationRule_CheckDeprecatedEmitters::CheckValidity(const FNiagaraValidationContext& Context, TArray& OutResults) const { // TODO(ME) Removed validity check before we delete the class } void UNiagaraValidationRule_LWC::CheckValidity(const FNiagaraValidationContext& Context, TArray& Results) const { const UNiagaraSettings* Settings = GetDefault(); UNiagaraSystem& System = Context.ViewModel->GetSystem(); if (!System.SupportsLargeWorldCoordinates()) { return; } // gather all the modules in the system, excluding localspace emitters TArray AllModules; AllModules.Append(NiagaraValidation::GetStackEntries(Context.ViewModel->GetSystemStackViewModel())); TArray> EmitterHandleViewModels = Context.ViewModel->GetEmitterHandleViewModels(); for (TSharedRef EmitterHandleModel : EmitterHandleViewModels) { const FVersionedNiagaraEmitterData* EmitterData = EmitterHandleModel->GetEmitterHandle()->GetEmitterData(); if (EmitterHandleModel->GetIsEnabled() && EmitterData && EmitterData->bLocalSpace == false) { AllModules.Append(NiagaraValidation::GetStackEntries(EmitterHandleModel.Get().GetEmitterStackViewModel())); } } for (UNiagaraStackModuleItem* Module : AllModules) { TArray StackInputs; Module->GetParameterInputs(StackInputs); for (UNiagaraStackFunctionInput* Input : StackInputs) { if (Input->GetInputType() == FNiagaraTypeDefinition::GetPositionDef()) { // check if any position inputs are set locally to absolute values if (Input->GetValueMode() == UNiagaraStackFunctionInput::EValueMode::Local) { FNiagaraValidationResult Result(ENiagaraValidationSeverity::Warning, FText::Format(LOCTEXT("LocalPosInputSummary", "Input '{0}' set to absolute value"), Input->GetDisplayName()), LOCTEXT("LocalPosInputDescription", "Position attributes should never be set to an absolute values, because they will be offset when using large world coordinates.\nInstead, set them relative to a known position like Engine.Owner.Position."), Input); Results.Add(Result); } // check if the linked dynamic input script outputs a vector if (Input->GetValueMode() == UNiagaraStackFunctionInput::EValueMode::Dynamic && Input->GetDynamicInputNode() && Settings->bEnforceStrictStackTypes) { if (UNiagaraScriptSource* DynamicInputSource = Cast(Input->GetDynamicInputNode()->GetFunctionScriptSource())) { TArray OutNodes; DynamicInputSource->NodeGraph->GetOutputNodeVariables(OutNodes); for (const FNiagaraVariable& OutVariable : OutNodes) { if (OutVariable.GetType() == FNiagaraTypeDefinition::GetVec3Def()) { FTextFormat DescriptionFormat = LOCTEXT("VecDILinkedToPosInputDescription", "The position input {0} is linked to a dynamic input that outputs a vector.\nPlease use a dynamic input that outputs a position instead or explicitly convert the vector to a position type."); FNiagaraValidationResult Result(ENiagaraValidationSeverity::Warning, LOCTEXT("VecDILinkedToPosInputSummary", "Position input is linked to a vector output"), FText::Format(DescriptionFormat, Input->GetDisplayName()), Input); Results.Add(Result); } } } } // check if the linked input variable is a vector if (Input->GetValueMode() == UNiagaraStackFunctionInput::EValueMode::Linked && Settings->bEnforceStrictStackTypes) { FNiagaraVariable VectorVar(FNiagaraTypeDefinition::GetVec3Def(), Input->GetLinkedParameterValue().GetName()); const UNiagaraGraph* NiagaraGraph = Input->GetInputFunctionCallNode().GetNiagaraGraph(); // we check if metadata for a vector attribute with the linked name exists in the emitter/system script graph. Not 100% correct, but it needs to be fast and a few false negatives are acceptable. if (NiagaraGraph && NiagaraGraph->GetMetaData(VectorVar).IsSet()) { FNiagaraValidationResult Result(ENiagaraValidationSeverity::Warning, FText::Format(LOCTEXT("PositionLinkedVectorSummary", "Input '{0}' is linked to a vector attribute"), Input->GetDisplayName()), LOCTEXT("PositionLinkedVectorDescription", "Position types should only be linked to position attributes. In this case, it is linked to a vector attribute and the implicit conversion can cause problems with large world coordinates."), Input); Results.Add(Result); } } } } } } void UNiagaraValidationRule_NoOpaqueRenderMaterial::CheckValidity(const FNiagaraValidationContext& Context, TArray& Results) const { // check that we are called from a valid module UNiagaraStackModuleItem* SourceModule = Cast(Context.Source); if (SourceModule && SourceModule->GetIsEnabled() && SourceModule->GetEmitterViewModel()) { static const FName NAME_GPUCollisionType("GPU Collision Type"); static const FName NAME_ZDepthQueryType("Z Depth Query Type"); // search for the right emitter view model for (const TSharedRef& EmitterHandleModel : Context.ViewModel->GetEmitterHandleViewModels()) { FVersionedNiagaraEmitterData* EmitterData = EmitterHandleModel->GetEmitterHandle()->GetEmitterData(); if (EmitterHandleModel->GetIsEnabled() && EmitterData && EmitterData->SimTarget == ENiagaraSimTarget::GPUComputeSim && EmitterHandleModel->GetEmitterViewModel() == SourceModule->GetEmitterViewModel()) { // Note: for these BP driven enums we can't compare the values TOptional GPUCollisionType = NiagaraValidation::GetModuleStaticInt32Value(SourceModule, NAME_GPUCollisionType); if (!GPUCollisionType.IsSet() || GPUCollisionType.GetValue() != 0) { continue; } TOptional ZDepthQueryType = NiagaraValidation::GetModuleStaticInt32Value(SourceModule, NAME_ZDepthQueryType); if (!ZDepthQueryType.IsSet() || ZDepthQueryType.GetValue() != 0) { continue; } // check the renderers TArray RendererItems = NiagaraValidation::GetStackEntries(EmitterHandleModel->GetEmitterStackViewModel()); for (UNiagaraStackRendererItem* Renderer : RendererItems) { if (UNiagaraRendererProperties* RendererProperties = Renderer->GetRendererProperties()) { TArray OutMaterials; RendererProperties->GetUsedMaterials(nullptr, OutMaterials); for (UMaterialInterface* Material : OutMaterials) { if (!Material) { continue; } if (IsOpaqueOrMaskedBlendMode(*Material)) { FText Description = LOCTEXT("NoOpaqueRenderMaterialDescription", "This renderer uses a material with a masked or opaque blend mode, which writes to the depth buffer.\nThis will cause conflicts when the collision module also uses depth buffer collisions."); FNiagaraValidationResult Result(ENiagaraValidationSeverity::Warning, FText::Format(LOCTEXT("NoOpaqueRenderMaterialSummary", "Renderer '{0}' has an opaque material"), Renderer->GetDisplayName()), Description, Renderer); //Add autofix to switch to distance field collisions if possible static const auto* CVarGenerateMeshDistanceFields = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.GenerateMeshDistanceFields")); if (CVarGenerateMeshDistanceFields != nullptr && CVarGenerateMeshDistanceFields->GetValueOnGameThread() > 0) { FNiagaraValidationFix& DisableRendererFix = Result.Fixes.AddDefaulted_GetRef(); DisableRendererFix.Description = LOCTEXT("SwitchCollisionFix", "Change collision type to distance fields"); TWeakObjectPtr WeakSourceModule = SourceModule; DisableRendererFix.FixDelegate = FNiagaraValidationFixDelegate::CreateLambda( [WeakSourceModule]() { if (UNiagaraStackModuleItem* CollisionModule = WeakSourceModule.Get()) { NiagaraValidation::SetModuleStaticInt32Value(CollisionModule, NAME_GPUCollisionType, 1); } }); } Results.Add(Result); } } } } } } } } void UNiagaraValidationRule_NoFixedDeltaTime::CheckValidity(const FNiagaraValidationContext& Context, TArray& OutResults) const { // check to see if we're called from a module or the effect type if (UNiagaraStackModuleItem* SourceModule = Cast(Context.Source)) { if (SourceModule->GetIsEnabled()) { UNiagaraSystem& System = SourceModule->GetSystemViewModel()->GetSystem(); if (System.HasFixedTickDelta()) { OutResults.Emplace( ENiagaraValidationSeverity::Warning, LOCTEXT("NoFixedDeltaTimeModule", "Module does not support fixed tick delta time"), LOCTEXT("NoFixedDeltaTimeModuleDetailed", "This system uses a fixed tick delta time, which means it might tick multiple times per frame or might skip ticks depending on the global tick rate.\nModules that depend on external assets such as render targets or collision data will NOT work correctly when their tick is different from the engine tick.\nConsider disabling the fixed tick delta time."), SourceModule ); } } } else { UNiagaraSystem& System = Context.ViewModel->GetSystem(); if (System.HasFixedTickDelta()) { OutResults.Emplace( ENiagaraValidationSeverity::Error, LOCTEXT("NoFixedDeltaTime", "Effect type does not allow fixed tick delta time"), LOCTEXT("NoFixedDeltaTimeDetailed", "This system uses a fixed tick delta time, which means it might tick multiple times per frame or might skip ticks depending on the global tick rate.\nThe selected effect type does not allow fixed tick delta times."), NiagaraValidation::GetStackEntry(Context.ViewModel->GetSystemStackViewModel()) ); } } } void UNiagaraValidationRule_SimulationStageBudget::CheckValidity(const FNiagaraValidationContext& Context, TArray& OutResults) const { for (const TSharedRef& EmitterHandleModel : Context.ViewModel->GetEmitterHandleViewModels()) { // Skip disabled if ( EmitterHandleModel->GetIsEnabled() == false ) { continue; } // Simulation stages are GPU only currently FVersionedNiagaraEmitterData* EmitterData = EmitterHandleModel.Get().GetEmitterHandle()->GetEmitterData(); if (EmitterData == nullptr) { continue; } if ( EmitterData->SimTarget != ENiagaraSimTarget::GPUComputeSim ) { continue; } int32 TotalIterations = 0; int32 TotalEnabledStages = 0; for ( UNiagaraSimulationStageBase* SimStageBase : EmitterData->GetSimulationStages() ) { UNiagaraSimulationStageGeneric* SimStage = Cast(SimStageBase); if ( SimStage == nullptr || SimStage->bEnabled == false ) { continue; } const int32 StageNumIterations = SimStage->NumIterations.GetDefaultValue(); ++TotalEnabledStages; TotalIterations += StageNumIterations; if ( bMaxIterationsPerStageEnabled && StageNumIterations > MaxIterationsPerStage) { UNiagaraStackEmitterPropertiesItem* EmitterProperties = NiagaraValidation::GetStackEntry(EmitterHandleModel.Get().GetEmitterStackViewModel()); OutResults.Emplace( Severity, FText::Format(LOCTEXT("SimStageTooManyIterationsFormat", "Simulation Stage '{0}' has too many iterations"), FText::FromName(SimStage->SimulationStageName)), FText::Format(LOCTEXT("SimStageTooManyIterationsDetailedFormat", "Simulation Stage '{0}' has {1} iterations and we only allow {2}"), FText::FromName(SimStage->SimulationStageName), FText::AsNumber(StageNumIterations), FText::AsNumber(MaxIterationsPerStage)), EmitterProperties ); } } if ( bMaxTotalIterationsEnabled && TotalIterations > MaxTotalIterations ) { UNiagaraStackEmitterPropertiesItem* EmitterProperties = NiagaraValidation::GetStackEntry(EmitterHandleModel.Get().GetEmitterStackViewModel()); OutResults.Emplace( Severity, LOCTEXT("SimStageTooManyTotalIterationsFormat", "Emitter has too many total simulation stage iterations"), FText::Format(LOCTEXT("SimStageTooManyTotalIterationsDetailedFormat", "Emitter has {0} total simulation stage iterations and we only allow {1}"), FText::AsNumber(TotalIterations), FText::AsNumber(MaxTotalIterations)), EmitterProperties ); } if ( bMaxSimulationStagesEnabled && TotalEnabledStages > MaxSimulationStages ) { UNiagaraStackEmitterPropertiesItem* EmitterProperties = NiagaraValidation::GetStackEntry(EmitterHandleModel.Get().GetEmitterStackViewModel()); OutResults.Emplace( Severity, LOCTEXT("TooManySimStagesFormat", "Emitter has too many simulation stages"), FText::Format(LOCTEXT("TooManySimStagesDetailedFormat", "Emitter has {0} simulation stages active and we only allow {1}"), FText::AsNumber(TotalEnabledStages), FText::AsNumber(MaxSimulationStages)), EmitterProperties ); } } } void UNiagaraValidationRule_TickDependencyCheck::CheckValidity(const FNiagaraValidationContext& Context, TArray& OutResults) const { if (!bCheckActorComponentInterface && !bCheckCameraDataInterface && !bCheckSkeletalMeshInterface) { return; } UNiagaraSystem* NiagaraSystem = &Context.ViewModel->GetSystem(); if (!NiagaraSystem->bRequireCurrentFrameData) { return; } if (EffectTypesToExclude.Contains(TSoftObjectPtr(NiagaraSystem->GetEffectType()))) { return; } TSet VisitedDIs; NiagaraSystem->ForEachScript( [&](UNiagaraScript* NiagaraScript) { for ( const FNiagaraScriptResolvedDataInterfaceInfo& ResolvedDI : NiagaraScript->GetResolvedDataInterfaces() ) { // Have we already encounted this DI? UNiagaraDataInterface* RuntimeDI = ResolvedDI.ResolvedDataInterface; if ( VisitedDIs.Contains(RuntimeDI) ) { continue; } VisitedDIs.Add(RuntimeDI); // Should we generate issues for this DI? bool bWarnTickDependency = false; if (UNiagaraDataInterfaceCamera* CameraDataInterface = Cast(RuntimeDI)) { bWarnTickDependency = bCheckCameraDataInterface && CameraDataInterface->bRequireCurrentFrameData; } else if (UNiagaraDataInterfaceSkeletalMesh* SkeletalMeshDataInterface = Cast(RuntimeDI)) { bWarnTickDependency = bCheckSkeletalMeshInterface && SkeletalMeshDataInterface->bRequireCurrentFrameData; } else if (UNiagaraDataInterfaceActorComponent* ActorComponentDataInterface = Cast(RuntimeDI)) { bWarnTickDependency = bCheckActorComponentInterface && ActorComponentDataInterface->bRequireCurrentFrameData; } if (bWarnTickDependency == false) { continue; } // Generate issue UObject* StackObject = nullptr; if (ResolvedDI.ResolvedSourceEmitterName.Len() > 0) { for (const TSharedRef& EmitterViewModel : Context.ViewModel->GetEmitterHandleViewModels()) { if (EmitterViewModel->GetName() == FName(ResolvedDI.ResolvedSourceEmitterName)) { StackObject = NiagaraValidation::GetStackEntry(EmitterViewModel->GetEmitterStackViewModel()); break; } } } else { StackObject = NiagaraValidation::GetStackEntry(Context.ViewModel->GetSystemStackViewModel()); } if (StackObject == nullptr) { continue; } const FText DIClassText = FText::FromName(RuntimeDI->GetClass()->GetFName()); const FText DIVariableText = FText::FromName(ResolvedDI.Name); FNiagaraValidationResult& ValiationResult = OutResults.Emplace_GetRef( Severity, LOCTEXT("TickDependencyCheckFormat", "Performance issue due to late ticking which may cause waits on the game thread."), FText::Format(LOCTEXT("TickDependencyCheckDetailedFormat", "'{0}' has a tick dependency that can removed by unchecking 'RequireCurrentFrameData' on the data interface. This could introduce a frame of latency but will allow the system to execute immediatly in the frame. Parameter Name '{1}'."), DIClassText, DIVariableText), StackObject ); ValiationResult.Fixes.Emplace( FNiagaraValidationFix( LOCTEXT("TickDependencyCheckFix", "Disable RequireCurrentFrameData in System Properties"), FNiagaraValidationFixDelegate::CreateLambda( [WeakNiagaraSystem=MakeWeakObjectPtr(NiagaraSystem)]() { if (UNiagaraSystem* Sys = WeakNiagaraSystem.Get()) { const FScopedTransaction Transaction(LOCTEXT("FixtSystemRequireCurrentFrameData", "System Require Current Frame Data Disabled")); Sys->Modify(); Sys->bRequireCurrentFrameData = false; } } ) ) ); } } ); } void UNiagaraValidationRule_UserDataInterfaces::CheckValidity(const FNiagaraValidationContext& Context, TArray& OutResults) const { UNiagaraSystem* NiagaraSystem = &Context.ViewModel->GetSystem(); const FNiagaraUserRedirectionParameterStore& ExposedParameters = NiagaraSystem->GetExposedParameters(); if (ExposedParameters.GetDataInterfaces().Num() == 0) { return; } UObject* StackObject = NiagaraValidation::GetStackEntry(Context.ViewModel->GetSystemStackViewModel()); for (const FNiagaraVariableWithOffset& Variable : ExposedParameters.ReadParameterVariables()) { if (Variable.IsDataInterface() == false) { continue; } UClass* DIClass = Variable.GetType().GetClass(); if (BannedDataInterfaces.Num() > 0 && !BannedDataInterfaces.Contains(DIClass)) { continue; } if (AllowDataInterfaces.Num() > 0 && AllowDataInterfaces.Contains(DIClass)) { continue; } if (bOnlyIncludeExposedUObjects && !NiagaraValidation::StructContainsUObjectProperty(DIClass)) { continue; } const FText DIClassText = FText::FromName(DIClass->GetFName()); const FText VariableText = FText::FromName(Variable.GetName()); OutResults.Emplace( Severity, FText::Format(LOCTEXT("UserDataInterfaceFormat", "User DataInterface '{0}' should be removed."), VariableText), FText::Format(LOCTEXT("UserDataInterfaceDetailedFormat", "DataInterface '{0}' type '{1}' may cause issues when exposed to UEFN and reduce performance when creating an instance. Consider moving to system level and use object parameter binding on the data interface instead."), VariableText, DIClassText), StackObject ); } } void UNiagaraValidationRule_SingletonModule::CheckValidity(const FNiagaraValidationContext& Context, TArray& OutResults) const { // check to see if we're called from a module if (UNiagaraStackModuleItem* SourceModule = Cast(Context.Source)) { if (SourceModule->GetIsEnabled()) { UNiagaraScript* ModuleScript = SourceModule->GetModuleNode().FunctionScript; TArray StackModuleItems = NiagaraValidation::GetAllStackEntriesInSystem(Context.ViewModel); for (UNiagaraStackModuleItem* Module : StackModuleItems) { // if another module in the same stack calls the same script, report it if (Module && Module != SourceModule && Module->GetIsEnabled() && Module->GetModuleNode().FunctionScript == ModuleScript && SourceModule->GetEmitterViewModel().Get() == Module->GetEmitterViewModel().Get()) { if (bCheckDetailedUsageContext) { ENiagaraScriptUsage ModuleAUsage = FNiagaraStackGraphUtilities::GetOutputNodeUsage(SourceModule->GetModuleNode()); ENiagaraScriptUsage ModuleBUsage = FNiagaraStackGraphUtilities::GetOutputNodeUsage(Module->GetModuleNode()); if (ModuleAUsage != ModuleBUsage) { continue; } } OutResults.Emplace_GetRef( Severity, LOCTEXT("SingletonModuleError", "Module can only be used once per stack"), LOCTEXT("SingletonModuleErrorDetailed", "This module is intended to be used as a singleton, so only once per emitter or system stack.\nThis is usually the case when there is a data dependency between modules because they share written attributes."), SourceModule ).Fixes.Emplace( FNiagaraValidationFix( LOCTEXT("SingletonModuleErrorFix", "Disable module"), FNiagaraValidationFixDelegate::CreateLambda( [WeakSourceModule=MakeWeakObjectPtr(SourceModule)]() { if (UNiagaraStackModuleItem* StackModule = WeakSourceModule.Get()) { StackModule->SetEnabled(false); } } ) ) ); } } } } } void UNiagaraValidationRule_NoMapForOnCpu::CheckValidity(const FNiagaraValidationContext& Context, TArray& OutResults) const { // gather all the modules in the system TArray AllModules; AllModules.Append(NiagaraValidation::GetStackEntries(Context.ViewModel->GetSystemStackViewModel())); TArray> EmitterHandleViewModels = Context.ViewModel->GetEmitterHandleViewModels(); for (TSharedRef EmitterHandleModel : EmitterHandleViewModels) { if (EmitterHandleModel->GetIsEnabled()) { AllModules.Append(NiagaraValidation::GetStackEntries(EmitterHandleModel.Get().GetEmitterStackViewModel())); } } for (UNiagaraStackModuleItem* Module : AllModules) { ENiagaraScriptUsage ScriptUsage = Module->GetOutputNode()->GetUsage(); FNiagaraEmitterViewModel* EmitterViewModel = Module->GetEmitterViewModel().Get(); FVersionedNiagaraEmitterData* EmitterData = EmitterViewModel ? EmitterViewModel->GetEmitter().GetEmitterData() : nullptr; if (!EmitterData) { continue; } if (EmitterData->SimTarget == ENiagaraSimTarget::GPUComputeSim && FNiagaraUtilities::ConvertScriptUsageToStaticSwitchContext(ScriptUsage) == ENiagaraScriptContextStaticSwitch::Particle) { // modules used in gpu scripts are ignored continue; } if (UNiagaraGraph* Graph = Module->GetModuleNode().GetCalledGraph()) { FObjectKey GraphKey(Graph); FGraphCheckResult& CheckResult = CachedResults.FindOrAdd(GraphKey); if (CheckResult.ChangeID != Graph->GetChangeID()) { CheckResult.ChangeID = Graph->GetChangeID(); CheckResult.bContainsMapForNode = false; TArray TraversalNodes; Graph->BuildTraversal(TraversalNodes, ENiagaraScriptUsage::Module, FGuid()); for (UNiagaraNode* TraversalNode : TraversalNodes) { if (TraversalNode->IsA() || TraversalNode->IsA()) { CheckResult.bContainsMapForNode = true; break; } } } if (CheckResult.bContainsMapForNode) { OutResults.Emplace( Severity, LOCTEXT("NoMapForOnCpu", "Map for node doesn't work in cpu scripts"), LOCTEXT("NoMapForOnCpuDetailed", "This module contains a map for node, which does not work yet for cpu scripts.\nMap for nodes only work in particle gpu scripts. System and emitter scripts always run on the cpu."), Module ); } } } } void UNiagaraValidationRule_ModuleSimTargetRestriction::CheckValidity(const FNiagaraValidationContext& Context, TArray& OutResults) const { // check to see if we're called from a module UNiagaraStackModuleItem* SourceModule = Cast(Context.Source); if ( SourceModule == nullptr || !SourceModule->GetIsEnabled()) { return; } const ENiagaraScriptUsage ScriptUsage = SourceModule->GetOutputNode()->GetUsage(); // system and emitter scripts are always cpu scripts if (FNiagaraUtilities::ConvertScriptUsageToStaticSwitchContext(ScriptUsage) != ENiagaraScriptContextStaticSwitch::Particle && SupportedSimTarget == ENiagaraSimTarget::GPUComputeSim) { OutResults.Emplace( Severity, LOCTEXT("SimTargetModuleCpuError", "This module only supports gpu sim targets"), LOCTEXT("SimTargetModuleCpuDetailedError", "This module is used in a system or emitter script (which are always executed on the cpu), but the module needs to run on the gpu. Place it in a particle script or simulation stage of a gpu emitter."), SourceModule ); return; } TSharedPtr EmitterViewModel = SourceModule->GetEmitterViewModel(); if (!EmitterViewModel.IsValid()) { return; } FVersionedNiagaraEmitterData* EmitterData = EmitterViewModel->GetEmitter().GetEmitterData(); if (!EmitterData) { return; } ENiagaraSimTarget SimTarget = EmitterViewModel->GetEmitter().GetEmitterData()->SimTarget; if (SimTarget != SupportedSimTarget) { FNiagaraValidationResult& Result = OutResults.Emplace_GetRef( Severity, LOCTEXT("SimTargetModuleError", "Module is not compatible with the current emitter sim target"), LOCTEXT("SimTargetModuleErrorDetailed", "This module has a restriction on the emitter sim target and can't run on both cpu/gpu emitters."), SourceModule ); Result.Fixes.Emplace( FNiagaraValidationFix( FText::Format(LOCTEXT("SimTargetModuleErrorFix", "Change emitter sim target to {0}"), FText::FromString(SupportedSimTarget == ENiagaraSimTarget::CPUSim ? "CPUSim" : "GPUComputeSim")), FNiagaraValidationFixDelegate::CreateLambda( [WeakSourceModule=MakeWeakObjectPtr(SourceModule), NewSimTarget=SupportedSimTarget]() { if (UNiagaraStackModuleItem* StackModule = WeakSourceModule.Get()) { TSharedPtr EmitterViewModel = StackModule->GetEmitterViewModel(); if (EmitterViewModel.IsValid()) { FVersionedNiagaraEmitter VersionedEmitter = EmitterViewModel->GetEmitter(); if (FVersionedNiagaraEmitterData* VersionedEmitterData = VersionedEmitter.GetEmitterData()) { const FScopedTransaction Transaction(LOCTEXT("ChangeSimTarget", "Change emitter sim target")); VersionedEmitter.Emitter->Modify(); VersionedEmitterData->SimTarget = NewSimTarget; FProperty* SimTargetProperty = FindFProperty(FVersionedNiagaraEmitterData::StaticStruct(), GET_MEMBER_NAME_CHECKED(FVersionedNiagaraEmitterData, SimTarget)); FPropertyChangedEvent PropertyChangedEvent(SimTargetProperty); VersionedEmitter.Emitter->PostEditChangeVersionedProperty(PropertyChangedEvent, VersionedEmitter.Version); UNiagaraSystem::RequestCompileForEmitter(VersionedEmitter); } } } } ) ) ); Result.Fixes.Emplace( FNiagaraValidationFix( LOCTEXT("SimTargetModuleErrorFixDisable", "Disable module"), FNiagaraValidationFixDelegate::CreateLambda( [WeakSourceModule=MakeWeakObjectPtr(SourceModule)]() { if (UNiagaraStackModuleItem* StackModule = WeakSourceModule.Get()) { StackModule->SetEnabled(false); } } ) ) ); } } void UNiagaraValidationRule_MaterialUsage::CheckValidity(const FNiagaraValidationContext& Context, TArray& OutResults) const { TArray> EmitterHandleViewModels = Context.ViewModel->GetEmitterHandleViewModels(); for (TSharedRef EmitterHandleModel : EmitterHandleViewModels) { if (EmitterHandleModel->GetIsEnabled() == false) { continue; } FVersionedNiagaraEmitterData* EmitterData = EmitterHandleModel.Get().GetEmitterHandle()->GetEmitterData(); if (EmitterData == nullptr) { continue; } EmitterData->ForEachRenderer( [&OutResults, EmitterHandleModel, this](UNiagaraRendererProperties* RendererProperties) { TArray Feedback; RendererProperties->GetRendererMaterialUsageFeedback(Feedback); if (Feedback.Num() == 0) { return; } OutResults.Emplace( FailedUsageSeverity, LOCTEXT("MissingMaterialUsage_Description", "Material usage flags are not set correctly."), FText::Format(LOCTEXT("MissingMaterialUsage", "Materials on renderer {0} are missing the usage flag required to render with."), FText::FromName(RendererProperties->GetFName())), NiagaraValidation::GetRendererStackItem(EmitterHandleModel.Get().GetEmitterStackViewModel(), RendererProperties) ); } ); } } #undef LOCTEXT_NAMESPACE