├── .gitignore ├── Config └── FilterPlugin.ini ├── CowNodes.uplugin ├── Docs ├── Autowire.gif ├── LoadAssetProfile.png ├── NodeIntro.gif └── PromotedReturn.gif ├── LICENSE ├── README.md └── Source └── CowNodes ├── CowNodes.build.cs ├── Private ├── CowCompilerUtilities.cpp ├── CowNodes.cpp └── K2Node_CowCreateWidgetAsync.cpp └── Public ├── CowCompilerUtilities.h └── K2Node_CowCreateWidgetAsync.h /.gitignore: -------------------------------------------------------------------------------- 1 | apr_venv 2 | 3 | # node.js modules 4 | node_modules/ 5 | .npmrc 6 | 7 | # misc 8 | .vscode 9 | .history 10 | .DS_Store 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | *.log 15 | .firebase 16 | old 17 | config.json 18 | .env 19 | *.db 20 | backtrace_state.json 21 | scratch 22 | 23 | # backend 24 | .serverless 25 | *.code-workspace 26 | .dynamodb 27 | test-client 28 | dynamodb-data-mapper-js 29 | TODO.md 30 | config.yml 31 | 32 | # typescript build cache 33 | *.tsbuildinfo 34 | 35 | # webpack build cache 36 | *.webpackCache 37 | 38 | # typescript / build output folders 39 | dist 40 | 41 | # Visual Studio 2015 user specific files 42 | .vs/ 43 | 44 | # Visual Studio 2015 database file 45 | *.VC.db 46 | 47 | # JetBrains 48 | .idea/ 49 | 50 | # Compiled Object files 51 | *.slo 52 | *.lo 53 | *.o 54 | *.obj 55 | 56 | # Precompiled Headers 57 | *.gch 58 | *.pch 59 | 60 | # Compiled Dynamic libraries 61 | *.so 62 | *.dylib 63 | *.dll 64 | 65 | # Fortran module files 66 | *.mod 67 | 68 | # Compiled Static libraries 69 | *.lai 70 | *.la 71 | *.a 72 | *.lib 73 | *.pdb 74 | 75 | # Executables 76 | *.exe 77 | *.out 78 | *.app 79 | *.ipa 80 | 81 | # These project files can be generated by the engine 82 | *.xcodeproj 83 | *.sln 84 | *.suo 85 | *.opensdf 86 | *.sdf 87 | *.VC.opendb 88 | 89 | # VS Code Extension Generated files 90 | .favorites.json 91 | 92 | Binaries/ 93 | Intermediate/ 94 | Packages/ -------------------------------------------------------------------------------- /Config/FilterPlugin.ini: -------------------------------------------------------------------------------- 1 | [FilterPlugin] 2 | ; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and 3 | ; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively. 4 | ; 5 | ; Examples: 6 | ; /README.txt 7 | ; /Extras/... 8 | ; /Binaries/ThirdParty/*.dll 9 | -------------------------------------------------------------------------------- /CowNodes.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "Version": 1, 4 | "VersionName": "1.0", 5 | "FriendlyName": "Cow Nodes", 6 | "Description": "A collection of nodes by Oleksandr \"sleepCOW\" Ozerov that will hopefully make your life a little bit easier", 7 | "Category": "Other", 8 | "CreatedBy": "sleepCOW", 9 | "CreatedByURL": "olexthelake@gmail.com", 10 | "DocsURL": "https://github.com/sleepCOW/CowNodes", 11 | "MarketplaceURL": "", 12 | "CanContainContent": false, 13 | "IsBetaVersion": false, 14 | "IsExperimentalVersion": false, 15 | "Installed": false, 16 | "Modules": [ 17 | { 18 | "Name": "CowNodes", 19 | "Type": "UncookedOnly", 20 | "LoadingPhase": "Default" 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /Docs/Autowire.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleepCOW/CowNodes/ceb74183fdfad359dc842700bf2206d35316c76f/Docs/Autowire.gif -------------------------------------------------------------------------------- /Docs/LoadAssetProfile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleepCOW/CowNodes/ceb74183fdfad359dc842700bf2206d35316c76f/Docs/LoadAssetProfile.png -------------------------------------------------------------------------------- /Docs/NodeIntro.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleepCOW/CowNodes/ceb74183fdfad359dc842700bf2206d35316c76f/Docs/NodeIntro.gif -------------------------------------------------------------------------------- /Docs/PromotedReturn.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sleepCOW/CowNodes/ceb74183fdfad359dc842700bf2206d35316c76f/Docs/PromotedReturn.gif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 sleepCOW 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://img.shields.io/badge/UE-5.5.3-green) 2 | ![](https://img.shields.io/badge/UE-5.4.4-green) 3 | 4 | # About 5 | I was frankly tired of using `CreateWidgetAsync` with no convenient option to assign properties without introducing hard-ref. 6 | 7 | Hopefully the name `Cow Nodes` will become true someday and there will be more public nodes but everything needs to be started at some point. 8 | 9 | # List of nodes: 10 | 11 | - [`CowCreateWidgetAsync` - Ultimate Create Widget Async](#CowCreateWidgetAsync) 12 | 13 | # CowCreateWidgetAsync 14 | Improved Version of Epic's `CreateWidget` and `CreateWidgetAsync` (from `CommonGame`) 15 | 16 | This version combines the best of both worlds with general quality-of-life improvements 17 | ![Autowire](Docs/NodeIntro.gif) 18 | 19 | ## Key Features 20 | 21 | - **No Hard References**: Does not introduce a hard reference to the selected widget class in packaged game (similar to `CreateWidgetAsync`). 22 | ![Tested async loading](Docs/LoadAssetProfile.png) 23 | 24 | - **ExposedOnSpawn Parameters**: Displays any `ExposedOnSpawn` parameters (similar to `CreateWidget`). 25 | - **Automatic Handling of `ExposedOnSpawn` Variables**: Automatically adds new `ExposedOnSpawn` variables (which `CreateWidget` does not handle). 26 | - **Widget Class Pin Enhancements**: 27 | - Supports linked properties and propagates their class. 28 | - Example: 29 | 30 | Suppose you created a soft class property `MyClass` of type `WBP_BaseWidget` with an exposed member `Text` (so only `WBP_BaseWidget` and derived classes can be selected). 31 | 32 | If you connect `MyClass` property to the node, it will properly: 33 | 34 | 1. Display any exposed members (`Text` will be shown). 35 | 2. Set the return type of the node to `WBP_BaseWidget`. 36 | (Because the pin type already introduces a hard reference (`SoftClass` property to a type `X` is a hard reference to `X`) 37 | ![Autowire](Docs/PromotedReturn.gif) 38 | - **Optimal Return Value**: The return value is always the best possible option that avoids a hard reference. 39 | It will be either the first native class or the widget class if linked to a pin that introduced a hard reference. 40 | - **Improved Auto-Wiring**: Proper auto-wiring for `ReturnType` to avoid the common mistake of using the `Then` pin instead of `OnWidgetCompleted`. 41 | ![Autowire](Docs/Autowire.gif) 42 | - **User-Friendly Notes**: Generates informative notes when actions might cause errors. 43 | - Introduction of a hard reference. 44 | - Keeping default values of the base class when a pin of the base class is hooked, which may differ at runtime. 45 | - **Fix for `FKismetCompilerUtilities::GenerateAssignmentNodes`**: 46 | - Prevents `DynamicCast` from creating a hard reference through native properties with `BlueprintSetter`. 47 | - See `FCowCompilerUtilities::GenerateAssignmentNodes` for implementation details. 48 | 49 | ## Known Limitations 50 | 51 | 1. Works only with **EventGraph/Macro** (due to being async). 52 | 2. No support for **conflicting names** (e.g., an `ExposedVar` named `"SoftWidgetClass"` will cause an error during node compilation). 53 | 3. **One-frame delay** even if the soft reference is already loaded due to `LoadAsset` (TODO). 54 | 4. **Editor Limitation**: In the editor, the node holds a **hard reference** to the `WidgetClass`. 55 | This is required for pin generation and proper reloading when `WidgetClass` changes. 56 | -------------------------------------------------------------------------------- /Source/CowNodes/CowNodes.build.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Oleksandr "sleepCOW" Ozerov. All rights reserved. 2 | 3 | using UnrealBuildTool; 4 | 5 | public class CowNodes : ModuleRules 6 | { 7 | public CowNodes(ReadOnlyTargetRules Target) : base(Target) 8 | { 9 | PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; 10 | 11 | PublicIncludePaths.AddRange( 12 | new string[] { 13 | // ... add public include paths required here ... 14 | } 15 | ); 16 | 17 | 18 | PrivateIncludePaths.AddRange( 19 | new string[] { 20 | // ... add other private include paths required here ... 21 | } 22 | ); 23 | 24 | 25 | PublicDependencyModuleNames.AddRange( 26 | new string[] 27 | { 28 | "Core", 29 | // ... add other public dependencies that you statically link with here ... 30 | } 31 | ); 32 | 33 | 34 | PrivateDependencyModuleNames.AddRange( 35 | new string[] 36 | { 37 | "Projects", 38 | "UnrealEd", 39 | "UMG", 40 | "UMGEditor", 41 | "CoreUObject", 42 | "Engine", 43 | "BlueprintGraph", 44 | "Kismet", 45 | "KismetCompiler" 46 | // ... add private dependencies that you statically link with here ... 47 | } 48 | ); 49 | 50 | 51 | DynamicallyLoadedModuleNames.AddRange( 52 | new string[] 53 | { 54 | // ... add any modules that your module loads dynamically here ... 55 | } 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Source/CowNodes/Private/CowCompilerUtilities.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Oleksandr "sleepCOW" Ozerov. All rights reserved. 2 | 3 | #include "CowCompilerUtilities.h" 4 | 5 | #include "BlueprintCompilationManager.h" 6 | #include "K2Node_CallArrayFunction.h" 7 | #include "K2Node_CallFunction.h" 8 | #include "K2Node_DynamicCast.h" 9 | #include "K2Node_EnumLiteral.h" 10 | #include "KismetCompiler.h" 11 | #include "Kismet2/BlueprintEditorUtils.h" 12 | 13 | const UClass* FCowCompilerUtilities::GetFirstNativeClass(const UClass* Child) 14 | { 15 | const UClass* Result = Child; 16 | while (Result && !Result->HasAnyClassFlags(CLASS_Native)) 17 | { 18 | Result = Result->GetSuperClass(); 19 | } 20 | return Result; 21 | } 22 | 23 | UClass* FCowCompilerUtilities::GetFirstNativeClass(UClass* Child) 24 | { 25 | return const_cast(FCowCompilerUtilities::GetFirstNativeClass(const_cast(Child))); 26 | } 27 | 28 | UEdGraphPin* FCowCompilerUtilities::GenerateAssignmentNodes(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph, UK2Node* CallBeginSpawnNode, UEdGraphNode* SpawnNode, UEdGraphPin* CallBeginResult, const UClass* ForClass, const UEdGraphPin* CallBeginClassInput) 29 | { 30 | static const FName ObjectParamName(TEXT("Object")); 31 | static const FName ValueParamName(TEXT("Value")); 32 | static const FName PropertyNameParamName(TEXT("PropertyName")); 33 | 34 | const UEdGraphSchema_K2* Schema = CompilerContext.GetSchema(); 35 | UEdGraphPin* LastThen = CallBeginSpawnNode->GetThenPin(); 36 | 37 | // If the class input pin is linked, then 'ForClass' represents a base type, but the actual type might be a derived class 38 | // that won't get resolved until runtime. In that case, we can't use the base type's CDO to avoid generating assignment 39 | // statements for unlinked parameter inputs that match the base type's default value for those parameters, since the 40 | // derived type might store a different default value, which would otherwise be used if we don't explicitly assign it. 41 | const bool bIsClassInputPinLinked = CallBeginClassInput && CallBeginClassInput->LinkedTo.Num() > 0; 42 | 43 | // Create 'set var by name' nodes and hook them up 44 | for (int32 PinIdx = 0; PinIdx < SpawnNode->Pins.Num(); PinIdx++) 45 | { 46 | // Only create 'set param by name' node if this pin is linked to something 47 | UEdGraphPin* OrgPin = SpawnNode->Pins[PinIdx]; 48 | 49 | if (OrgPin->Direction == EGPD_Output) 50 | { 51 | continue; 52 | } 53 | 54 | if (!CallBeginSpawnNode->FindPin(OrgPin->PinName)) 55 | { 56 | FProperty* Property = FindFProperty(ForClass, OrgPin->PinName); 57 | // NULL property indicates that this pin was part of the original node, not the 58 | // class we're assigning to: 59 | if (!Property) 60 | { 61 | continue; 62 | } 63 | 64 | if (OrgPin->LinkedTo.Num() == 0) 65 | { 66 | FString DefaultValueErrorString = Schema->IsCurrentPinDefaultValid(OrgPin); 67 | if (!DefaultValueErrorString.IsEmpty()) 68 | { 69 | // Some types require a connection for assignment (e.g. arrays). 70 | continue; 71 | } 72 | // If the property is not editable in blueprint, always check the native CDO to handle cases like Instigator properly 73 | else if (!bIsClassInputPinLinked || Property->HasAnyPropertyFlags(CPF_DisableEditOnTemplate) || !Property->HasAnyPropertyFlags(CPF_Edit)) 74 | { 75 | // We don't want to generate an assignment node unless the default value 76 | // differs from the value in the CDO: 77 | FString DefaultValueAsString; 78 | 79 | if (!FBlueprintCompilationManager::GetDefaultValue(ForClass, Property, DefaultValueAsString)) 80 | { 81 | if (ForClass->ClassDefaultObject) 82 | { 83 | FBlueprintEditorUtils::PropertyValueToString(Property, (uint8*)ForClass->ClassDefaultObject.Get(), DefaultValueAsString); 84 | } 85 | } 86 | 87 | // First check the string representation of the default value 88 | if (Schema->DoesDefaultValueMatch(*OrgPin, DefaultValueAsString)) 89 | { 90 | continue; 91 | } 92 | 93 | FString UseDefaultValue; 94 | TObjectPtr UseDefaultObject = nullptr; 95 | FText UseDefaultText; 96 | constexpr bool bPreserveTextIdentity = true; 97 | 98 | // Next check if the converted default value would be the same to handle cases like None for object pointers 99 | Schema->GetPinDefaultValuesFromString(OrgPin->PinType, OrgPin->GetOwningNodeUnchecked(), DefaultValueAsString, UseDefaultValue, UseDefaultObject, UseDefaultText, bPreserveTextIdentity); 100 | 101 | if (OrgPin->DefaultValue.Equals(UseDefaultValue, ESearchCase::CaseSensitive) && OrgPin->DefaultObject == UseDefaultObject && OrgPin->DefaultTextValue.IdenticalTo(UseDefaultText)) 102 | { 103 | continue; 104 | } 105 | } 106 | } 107 | 108 | const FString& SetFunctionName = Property->GetMetaData(FBlueprintMetadata::MD_PropertySetFunction); 109 | if (!SetFunctionName.IsEmpty()) 110 | { 111 | UClass* NativeClass = FCowCompilerUtilities::GetFirstNativeClass(const_cast(ForClass)); // Fixed hard-ref to blueprint class 112 | 113 | UFunction* SetFunction = NativeClass->FindFunctionByName(*SetFunctionName); 114 | check(SetFunction); 115 | 116 | // Add a cast node so we can call the Setter function with a pin of the right class 117 | UK2Node_DynamicCast* CastNode = CompilerContext.SpawnIntermediateNode(SpawnNode, SourceGraph); 118 | CastNode->TargetType = NativeClass; // Fixed hard-ref to blueprint class 119 | CastNode->SetPurity(true); 120 | CastNode->AllocateDefaultPins(); 121 | CastNode->GetCastSourcePin()->MakeLinkTo(CallBeginResult); 122 | CastNode->NotifyPinConnectionListChanged(CastNode->GetCastSourcePin()); 123 | 124 | UK2Node_CallFunction* CallFuncNode = CompilerContext.SpawnIntermediateNode(SpawnNode, SourceGraph); 125 | CallFuncNode->SetFromFunction(SetFunction); 126 | CallFuncNode->AllocateDefaultPins(); 127 | 128 | // Connect this node into the exec chain 129 | Schema->TryCreateConnection(LastThen, CallFuncNode->GetExecPin()); 130 | LastThen = CallFuncNode->GetThenPin(); 131 | 132 | // Connect the new object to the 'object' pin 133 | UEdGraphPin* ObjectPin = Schema->FindSelfPin(*CallFuncNode, EGPD_Input); 134 | CastNode->GetCastResultPin()->MakeLinkTo(ObjectPin); 135 | 136 | // Move Value pin connections 137 | UEdGraphPin* SetFunctionValuePin = nullptr; 138 | for (UEdGraphPin* CallFuncPin : CallFuncNode->Pins) 139 | { 140 | if (!Schema->IsMetaPin(*CallFuncPin)) 141 | { 142 | check(CallFuncPin->Direction == EGPD_Input); 143 | SetFunctionValuePin = CallFuncPin; 144 | break; 145 | } 146 | } 147 | check(SetFunctionValuePin); 148 | 149 | CompilerContext.MovePinLinksToIntermediate(*OrgPin, *SetFunctionValuePin); 150 | } 151 | else if (FKismetCompilerUtilities::IsPropertyUsesFieldNotificationSetValueAndBroadcast(Property)) 152 | { 153 | // Add a cast node so we can call the Setter function with a pin of the right class 154 | //UK2Node_DynamicCast* CastNode = CompilerContext.SpawnIntermediateNode(SpawnNode, SourceGraph); 155 | //CastNode->TargetType = const_cast(ForClass); 156 | //CastNode->SetPurity(true); 157 | //CastNode->AllocateDefaultPins(); 158 | //CastNode->GetCastSourcePin()->MakeLinkTo(CallBeginResult); 159 | //CastNode->NotifyPinConnectionListChanged(CastNode->GetCastSourcePin()); 160 | 161 | FMemberReference MemberReference; 162 | MemberReference.SetFromField(Property, false); 163 | TTuple ExecThenPins = FKismetCompilerUtilities::GenerateFieldNotificationSetNode(CompilerContext, SourceGraph, SpawnNode, CallBeginResult, Property, MemberReference, false, false, Property->HasAllPropertyFlags(CPF_Net)); 164 | 165 | // Connect this node into the exec chain 166 | Schema->TryCreateConnection(LastThen, ExecThenPins.Get<0>()); 167 | LastThen = ExecThenPins.Get<1>(); 168 | } 169 | else if (UFunction* SetByNameFunction = Schema->FindSetVariableByNameFunction(OrgPin->PinType)) 170 | { 171 | UK2Node_CallFunction* SetVarNode = nullptr; 172 | if (OrgPin->PinType.IsArray()) 173 | { 174 | SetVarNode = CompilerContext.SpawnIntermediateNode(SpawnNode, SourceGraph); 175 | } 176 | else 177 | { 178 | SetVarNode = CompilerContext.SpawnIntermediateNode(SpawnNode, SourceGraph); 179 | } 180 | SetVarNode->SetFromFunction(SetByNameFunction); 181 | SetVarNode->AllocateDefaultPins(); 182 | 183 | // Connect this node into the exec chain 184 | Schema->TryCreateConnection(LastThen, SetVarNode->GetExecPin()); 185 | LastThen = SetVarNode->GetThenPin(); 186 | 187 | // Connect the new object to the 'object' pin 188 | UEdGraphPin* ObjectPin = SetVarNode->FindPinChecked(ObjectParamName); 189 | CallBeginResult->MakeLinkTo(ObjectPin); 190 | 191 | // Fill in literal for 'property name' pin - name of pin is property name 192 | UEdGraphPin* PropertyNamePin = SetVarNode->FindPinChecked(PropertyNameParamName); 193 | PropertyNamePin->DefaultValue = OrgPin->PinName.ToString(); 194 | 195 | UEdGraphPin* ValuePin = SetVarNode->FindPinChecked(ValueParamName); 196 | if (OrgPin->LinkedTo.Num() == 0 && 197 | OrgPin->DefaultValue != FString() && 198 | OrgPin->PinType.PinCategory == UEdGraphSchema_K2::PC_Byte && 199 | OrgPin->PinType.PinSubCategoryObject.IsValid() && 200 | OrgPin->PinType.PinSubCategoryObject->IsA()) 201 | { 202 | // Pin is an enum, we need to alias the enum value to an int: 203 | UK2Node_EnumLiteral* EnumLiteralNode = CompilerContext.SpawnIntermediateNode(SpawnNode, SourceGraph); 204 | EnumLiteralNode->Enum = CastChecked(OrgPin->PinType.PinSubCategoryObject.Get()); 205 | EnumLiteralNode->AllocateDefaultPins(); 206 | EnumLiteralNode->FindPinChecked(UEdGraphSchema_K2::PN_ReturnValue)->MakeLinkTo(ValuePin); 207 | 208 | UEdGraphPin* InPin = EnumLiteralNode->FindPinChecked(UK2Node_EnumLiteral::GetEnumInputPinName()); 209 | check( InPin ); 210 | InPin->DefaultValue = OrgPin->DefaultValue; 211 | } 212 | else 213 | { 214 | // For non-array struct pins that are not linked, transfer the pin type so that the node will expand an auto-ref that will assign the value by-ref. 215 | if (OrgPin->PinType.IsArray() == false && OrgPin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct && OrgPin->LinkedTo.Num() == 0) 216 | { 217 | ValuePin->PinType.PinCategory = OrgPin->PinType.PinCategory; 218 | ValuePin->PinType.PinSubCategory = OrgPin->PinType.PinSubCategory; 219 | ValuePin->PinType.PinSubCategoryObject = OrgPin->PinType.PinSubCategoryObject; 220 | CompilerContext.MovePinLinksToIntermediate(*OrgPin, *ValuePin); 221 | } 222 | else 223 | { 224 | // For interface pins we need to copy over the subcategory 225 | if (OrgPin->PinType.PinCategory == UEdGraphSchema_K2::PC_Interface) 226 | { 227 | ValuePin->PinType.PinSubCategoryObject = OrgPin->PinType.PinSubCategoryObject; 228 | } 229 | 230 | CompilerContext.MovePinLinksToIntermediate(*OrgPin, *ValuePin); 231 | SetVarNode->PinConnectionListChanged(ValuePin); 232 | } 233 | } 234 | } 235 | } 236 | } 237 | 238 | return LastThen; 239 | } 240 | -------------------------------------------------------------------------------- /Source/CowNodes/Private/CowNodes.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Oleksandr "sleepCOW" Ozerov. All rights reserved. 2 | 3 | #include "Modules/ModuleManager.h" 4 | 5 | IMPLEMENT_MODULE(FDefaultModuleImpl, CowNodes); -------------------------------------------------------------------------------- /Source/CowNodes/Private/K2Node_CowCreateWidgetAsync.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Oleksandr "sleepCOW" Ozerov. All rights reserved. 2 | 3 | 4 | #include "K2Node_CowCreateWidgetAsync.h" 5 | 6 | #include "BlueprintCompilationManager.h" 7 | #include "CowCompilerUtilities.h" 8 | #include "K2Node_CallFunction.h" 9 | #include "K2Node_LoadAsset.h" 10 | #include "KismetCompiler.h" 11 | #include "Blueprint/UserWidget.h" 12 | #include "Blueprint/WidgetBlueprintLibrary.h" 13 | #include "Kismet/KismetSystemLibrary.h" 14 | #include "Kismet2/BlueprintEditorUtils.h" 15 | 16 | #define LOCTEXT_NAMESPACE "Cow" 17 | 18 | UEdGraphPin* UK2Node_CowCreateWidgetAsync::GetSoftWidgetPin() const 19 | { 20 | return FindPinChecked(SoftWidgetClass, EGPD_Input); 21 | } 22 | 23 | void UK2Node_CowCreateWidgetAsync::ValidateSpawnVarPins(const FKismetCompilerContext& CompilerContext) const 24 | { 25 | const UEdGraphSchema_K2* Schema = CompilerContext.GetSchema(); 26 | 27 | auto DisplayNoteForPin = [&CompilerContext](UEdGraphPin* Pin) 28 | { 29 | CompilerContext.MessageLog.Note(*LOCTEXT("CowCreateWidgetAsync_DefaultValueNote", "@@ pin's default value is equal to base class's default value which may indicate an error.").ToString(), Pin); 30 | }; 31 | 32 | // Blacklist for our internal pins 33 | auto InternalPins = GetInternalPins(); 34 | 35 | for (UEdGraphPin* Pin : Pins) 36 | { 37 | // We need to check only pins generated for Exposed vars 38 | // + we only care if default value is edited, any linked pins are out of the validation context 39 | // because their values could be different at runtime 40 | if (Pin && Pin->Direction == EGPD_Input && Pin->LinkedTo.IsEmpty()) 41 | { 42 | if (InternalPins.Find(Pin) != INDEX_NONE) 43 | { 44 | continue; 45 | } 46 | 47 | FProperty* PropertyForPin = WidgetClassToSpawn->FindPropertyByName(Pin->PinName); 48 | if (!PropertyForPin) 49 | { 50 | continue; 51 | } 52 | 53 | FString DefaultValueErrorString = Schema->IsCurrentPinDefaultValid(Pin); 54 | if (!DefaultValueErrorString.IsEmpty()) 55 | { 56 | // Some types require a connection for assignment (e.g. arrays). 57 | continue; 58 | } 59 | // We don't want to generate an assignment node unless the default value 60 | // differs from the value in the CDO: 61 | FString DefaultValueAsString; 62 | if (!FBlueprintCompilationManager::GetDefaultValue(WidgetClassToSpawn, PropertyForPin, DefaultValueAsString)) 63 | { 64 | if (WidgetClassToSpawn->ClassDefaultObject) 65 | { 66 | FBlueprintEditorUtils::PropertyValueToString(PropertyForPin, (uint8*)WidgetClassToSpawn->ClassDefaultObject.Get(), DefaultValueAsString); 67 | } 68 | } 69 | 70 | // First check the string representation of the default value 71 | if (Schema->DoesDefaultValueMatch(*Pin, DefaultValueAsString)) 72 | { 73 | DisplayNoteForPin(Pin); 74 | continue; 75 | } 76 | 77 | FString UseDefaultValue; 78 | TObjectPtr UseDefaultObject = nullptr; 79 | FText UseDefaultText; 80 | constexpr bool bPreserveTextIdentity = true; 81 | 82 | // Next check if the converted default value would be the same to handle cases like None for object pointers 83 | Schema->GetPinDefaultValuesFromString(Pin->PinType, Pin->GetOwningNodeUnchecked(), DefaultValueAsString, UseDefaultValue, UseDefaultObject, UseDefaultText, bPreserveTextIdentity); 84 | if (Pin->DefaultValue.Equals(UseDefaultValue, ESearchCase::CaseSensitive) && Pin->DefaultObject == UseDefaultObject && Pin->DefaultTextValue.IdenticalTo(UseDefaultText)) 85 | { 86 | DisplayNoteForPin(Pin); 87 | continue; 88 | } 89 | } 90 | } 91 | } 92 | 93 | bool UK2Node_CowCreateWidgetAsync::ValidateSpawnVarPinsNameConflicts(const FKismetCompilerContext& CompilerContext) const 94 | { 95 | // If any exposed pins has conflicting name with internal pins it will cause problems 96 | // Pins won't be shown 97 | // Generation of assignments will be wrong 98 | auto InternalPins = GetInternalPinNames(); 99 | check(WidgetClassToSpawn); 100 | 101 | for (TFieldIterator PropertyIt(WidgetClassToSpawn, EFieldIteratorFlags::IncludeSuper); PropertyIt; ++PropertyIt) 102 | { 103 | FProperty* Property = *PropertyIt; 104 | const bool bIsDelegate = Property->IsA(FMulticastDelegateProperty::StaticClass()); 105 | const bool bIsExposedToSpawn = UEdGraphSchema_K2::IsPropertyExposedOnSpawn(Property); 106 | const bool bIsSettableExternally = !Property->HasAnyPropertyFlags(CPF_DisableEditOnInstance); 107 | 108 | if( bIsExposedToSpawn && 109 | !Property->HasAnyPropertyFlags(CPF_Parm) && 110 | bIsSettableExternally && 111 | Property->HasAllPropertyFlags(CPF_BlueprintVisible) && 112 | !bIsDelegate && 113 | FBlueprintEditorUtils::PropertyStillExists(Property)) 114 | { 115 | int32 Index = InternalPins.Find(Property->GetFName()); 116 | if (const bool bPropertyNameIsConflictingWithInternalPins = Index != INDEX_NONE) 117 | { 118 | CompilerContext.MessageLog.Error(*FString::Printf(TEXT("@@ property %s from type %s has conflicting name with internal pin %s"), 119 | *Property->GetName(), *GetNameSafe(WidgetClassToSpawn), *InternalPins[Index].ToString()), this); 120 | return false; 121 | } 122 | } 123 | } 124 | 125 | return true; 126 | } 127 | 128 | FText UK2Node_CowCreateWidgetAsync::GetBaseNodeTitle() const 129 | { 130 | return LOCTEXT("CreateWidget", "Cow Create Widget Async"); 131 | } 132 | 133 | FText UK2Node_CowCreateWidgetAsync::GetNodeTitleFormat() const 134 | { 135 | return LOCTEXT("CreateWidget", "Cow Create {ClassName} Widget Async"); 136 | } 137 | 138 | void UK2Node_CowCreateWidgetAsync::TryCreateOnWidgetCreatedPin() 139 | { 140 | if (FindPin(WidgetCreated, EGPD_Output) == nullptr) 141 | { 142 | FEdGraphPinType PinType; 143 | PinType.PinCategory = UEdGraphSchema_K2::PC_Exec; 144 | CreatePin(EGPD_Output, PinType, WidgetCreated, 2); 145 | } 146 | } 147 | 148 | void UK2Node_CowCreateWidgetAsync::PostReconstructNode() 149 | { 150 | Super::PostReconstructNode(); 151 | 152 | // When node is refreshed node's lifecycle doesn't call any events we defined to fixup the state 153 | // So we need also to handle it separately in here 154 | OnSoftWidgetClassChanged(); 155 | } 156 | 157 | void UK2Node_CowCreateWidgetAsync::PostLoad() 158 | { 159 | // Regenerate pins because 160 | // We could have changed dependant class while blueprint with the node was closed, therefore we missed OnChanged event 161 | if (HasValidBlueprint()) 162 | { 163 | OnSoftWidgetClassChanged(); 164 | } 165 | 166 | Super::PostLoad(); 167 | } 168 | 169 | void UK2Node_CowCreateWidgetAsync::BeginDestroy() 170 | { 171 | UnbindFromBlueprintChange(); 172 | 173 | Super::BeginDestroy(); 174 | } 175 | 176 | void UK2Node_CowCreateWidgetAsync::ReconstructNode() 177 | { 178 | Super::ReconstructNode(); 179 | 180 | UE_LOG(LogTemp, Log, TEXT("ReconstructNode")); 181 | } 182 | 183 | void UK2Node_CowCreateWidgetAsync::AllocateDefaultPins() 184 | { 185 | Super::AllocateDefaultPins(); 186 | 187 | // Hide "Class" pin from UK2Node_CreateWidget 188 | // It is used to reuse parent functionality to spawn ExposedOnSpawn pins from selected class 189 | UEdGraphPin* ClassPin = FindPin(WidgetClass, EGPD_Input); 190 | check(ClassPin); 191 | ClassPin->bHidden = true; 192 | 193 | FEdGraphPinType ExtensionVariableType; 194 | ExtensionVariableType.PinCategory = UEdGraphSchema_K2::PC_SoftClass; 195 | ExtensionVariableType.PinSubCategoryObject = UUserWidget::StaticClass(); 196 | CreatePin(EGPD_Input, ExtensionVariableType, SoftWidgetClass); 197 | 198 | TryCreateOnWidgetCreatedPin(); 199 | } 200 | 201 | void UK2Node_CowCreateWidgetAsync::ExpandNode(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) 202 | { 203 | // On purpose omitted Super::ExpandNode because it will duplicate a lot of ongoing logic 204 | UK2Node_ConstructObjectFromClass::ExpandNode(CompilerContext, SourceGraph); 205 | 206 | if (WidgetClassToSpawn == nullptr) 207 | { 208 | CompilerContext.MessageLog.Error(*LOCTEXT("CowCreateWidgetAsync_Error", "Create Widget Async node @@ must have a class specified.").ToString(), this); 209 | // we break exec links so this is the only error we get, don't want the CreateWidget node being considered and giving 'unexpected node' type warnings 210 | BreakAllNodeLinks(); 211 | return; 212 | } 213 | 214 | if (!ValidateSpawnVarPinsNameConflicts(CompilerContext)) 215 | { 216 | BreakAllNodeLinks(); 217 | return; 218 | } 219 | 220 | // Graph for better understanding implementation details: 221 | // 222 | // /> Then (Executed immediately after LoadAsset call) 223 | // LoadAsset -| 224 | // \> OnCompleted -> Cast to UUserWidget class -> Call UWidgetBlueprintLibrary::Create -> Generate assignments via SetPropertyByName -> OnWidgetCompleted pin 225 | // (because return is Object) 226 | // 227 | // Create intermediate nodes: 228 | // 1. LoadAsset node 229 | // 2. CallFunction to UKismetSystemLibrary::Conv_ObjectToClass 230 | // 3. CallFunction to UWidgetBlueprintLibrary::Create (taken from parent class) 231 | // 4. Spawn bunch of assignments via SetPropertyByName 232 | // 5. Hook last assignment to the OnWidgetCompleted pin 233 | // 234 | // Break all pins to this node because it was fake anything is passed via intermediate nodes 235 | 236 | const UEdGraphSchema_K2* Schema = CompilerContext.GetSchema(); 237 | 238 | UK2Node_LoadAsset* LoadAsset = CompilerContext.SpawnIntermediateNode(this, SourceGraph); 239 | LoadAsset->AllocateDefaultPins(); 240 | UEdGraphPin* LoadAsset_InputAsset = LoadAsset->FindPinChecked(LoadAsset_Input); 241 | UEdGraphPin* LoadAsset_OutputObject = LoadAsset->FindPinChecked(LoadAsset_Output); 242 | UEdGraphPin* LoadAsset_OutputCompleted = LoadAsset->FindPinChecked(UEdGraphSchema_K2::PN_Completed, EGPD_Output); 243 | 244 | UK2Node_CallFunction* Cast_ObjectToClass = CompilerContext.SpawnIntermediateNode(this, SourceGraph); 245 | Cast_ObjectToClass->FunctionReference.SetExternalMember(GET_FUNCTION_NAME_CHECKED(UKismetSystemLibrary, Conv_ObjectToClass), UKismetSystemLibrary::StaticClass()); 246 | Cast_ObjectToClass->AllocateDefaultPins(); 247 | UEdGraphPin* Cast_InputObject = Cast_ObjectToClass->FindPinChecked(FName(TEXT("Object")), EGPD_Input); 248 | UEdGraphPin* Cast_InputClass = Cast_ObjectToClass->FindPinChecked(FName(TEXT("Class")), EGPD_Input); 249 | UEdGraphPin* Cast_ReturnValue = Cast_ObjectToClass->GetReturnValuePin(); 250 | 251 | UEdGraphPin* This_InputSoftRef = GetSoftWidgetPin(); 252 | UEdGraphPin* This_OutputOnWidgetCreated = FindPinChecked(WidgetCreated, EGPD_Output); 253 | 254 | // 1. this.exec to LoadAsset.exec 255 | CompilerContext.MovePinLinksToIntermediate(*GetExecPin(), *LoadAsset->GetExecPin()); 256 | // 2. this.then to LoadAsset.then 257 | CompilerContext.MovePinLinksToIntermediate(*GetThenPin(), *LoadAsset->GetThenPin()); 258 | 259 | // 3. this.SoftWidgetClass to LoadAsset.Asset 260 | // We can select SoftWidgetClass to spawn in 2 ways 261 | // 262 | // a. We can connect some other pin to SoftWidgetClass pin 263 | // In such a case we need to do an additional conversation SoftClass -> SoftClassPath -> SoftObjectReference 264 | // Otherwise we cannot connect it to LoadAsset_Input 265 | if (!This_InputSoftRef->LinkedTo.IsEmpty()) 266 | { 267 | // Notify user if he introduced hard-ref through the linked pin 268 | UEdGraphPin* SoftWidgetSourcePin = This_InputSoftRef->LinkedTo[0]; 269 | if (UClass* SourceClass = Cast(SoftWidgetSourcePin->PinType.PinSubCategoryObject.Get())) 270 | { 271 | if (!SourceClass->HasAnyClassFlags(CLASS_Native)) 272 | { 273 | // Let's be honest - I won't localize it, so fuck this LOCTEXT macro :) 274 | CompilerContext.MessageLog.Note(*FString::Printf(TEXT("You introduced hard-ref to %s via @@ pin. Make sure it doesn't happened accidentally and you actually wanted it"), *SourceClass->GetName()), SoftWidgetSourcePin); 275 | } 276 | } 277 | 278 | UEdGraphPin* ConversationReturnPin = GenerateConvertToSoftObjectRef(CompilerContext, SourceGraph, This_InputSoftRef); 279 | ensureAlways(Schema->TryCreateConnection(ConversationReturnPin, LoadAsset_InputAsset)); 280 | 281 | // If we happen to connect SoftWidgetClass via Link to another pin (meaning at runtime the class may be different) 282 | // Give a note for any default values that aren't changed from the base class 283 | ValidateSpawnVarPins(CompilerContext); 284 | } 285 | // b. We selected class directly using drop-down class picker 286 | // The selected class is in Pin->DefaultValue and we can simply transfer the data 287 | else 288 | { 289 | CompilerContext.MovePinLinksToIntermediate(*This_InputSoftRef, *LoadAsset_InputAsset); 290 | } 291 | 292 | // 4. LoadAsset.Object to Cast.Object 293 | Cast_InputClass->DefaultObject = UUserWidget::StaticClass(); 294 | ensureAlways(Schema->TryCreateConnection(LoadAsset_OutputObject, Cast_InputObject)); 295 | 296 | UEdGraphPin* This_InputWorldContextPin = GetWorldContextPin(); 297 | UEdGraphPin* This_OwningPlayerPin = GetOwningPlayerPin(); 298 | UEdGraphPin* This_NodeResult = GetResultPin(); 299 | 300 | ////////////////////////////////////////////////////////////////////////// 301 | // create 'UWidgetBlueprintLibrary::Create' call node 302 | UK2Node_CallFunction* CallCreateNode = CompilerContext.SpawnIntermediateNode(this, SourceGraph); 303 | CallCreateNode->FunctionReference.SetExternalMember(GET_FUNCTION_NAME_CHECKED(UWidgetBlueprintLibrary, Create), UWidgetBlueprintLibrary::StaticClass()); 304 | CallCreateNode->AllocateDefaultPins(); 305 | 306 | UEdGraphPin* Create_InputExec = CallCreateNode->GetExecPin(); 307 | UEdGraphPin* Create_InputWorldContextPin = CallCreateNode->FindPinChecked(Create_InputWorldContextObject); 308 | UEdGraphPin* Create_InputWidgetTypePin = CallCreateNode->FindPinChecked(Create_InputWidgetType); 309 | UEdGraphPin* Create_InputOwningPlayerPin = CallCreateNode->FindPinChecked(Create_InputOwningPlayer); 310 | UEdGraphPin* Create_OutputResult = CallCreateNode->GetReturnValuePin(); 311 | 312 | // move this.completed to 'UWidgetBlueprintLibrary::Create' 313 | ensureAlways(Schema->TryCreateConnection(LoadAsset_OutputCompleted, Create_InputExec)); 314 | 315 | // 5. Cast.ReturnValue to Create.WidgetType 316 | Cast_ReturnValue->PinType = Create_InputWidgetTypePin->PinType; // (Type match required to connect pins) 317 | ensureAlways(Schema->TryCreateConnection(Cast_ReturnValue, Create_InputWidgetTypePin)); 318 | 319 | // Copy the world context connection from the spawn node to 'UWidgetBlueprintLibrary::Create' if necessary 320 | if ( This_InputWorldContextPin ) 321 | { 322 | CompilerContext.MovePinLinksToIntermediate(*This_InputWorldContextPin, *Create_InputWorldContextPin); 323 | } 324 | 325 | // Copy the 'Owning Player' connection from the spawn node to 'UWidgetBlueprintLibrary::Create' 326 | CompilerContext.MovePinLinksToIntermediate(*This_OwningPlayerPin, *Create_InputOwningPlayerPin); 327 | 328 | // Move result connection from spawn node to 'UWidgetBlueprintLibrary::Create' 329 | CompilerContext.MovePinLinksToIntermediate(*This_NodeResult, *Create_OutputResult); 330 | 331 | ////////////////////////////////////////////////////////////////////////// 332 | // create 'set var' nodes 333 | UEdGraphPin* LastThen = FCowCompilerUtilities::GenerateAssignmentNodes(CompilerContext, SourceGraph, CallCreateNode, this, Create_OutputResult, WidgetClassToSpawn, Create_InputWidgetTypePin); 334 | 335 | // Move 'then' connection from create widget node to the last 'then' 336 | CompilerContext.MovePinLinksToIntermediate(*This_OutputOnWidgetCreated, *LastThen); 337 | 338 | BreakAllNodeLinks(); 339 | } 340 | 341 | FText UK2Node_CowCreateWidgetAsync::GetNodeTitle(ENodeTitleType::Type TitleType) const 342 | { 343 | if (WidgetClassToSpawn != nullptr) 344 | { 345 | if (CachedNodeTitle.IsOutOfDate(this)) 346 | { 347 | FFormatNamedArguments Args; 348 | Args.Add(TEXT("ClassName"), WidgetClassToSpawn->GetDisplayNameText()); 349 | // FText::Format() is slow, so we cache this to save on performance 350 | CachedNodeTitle.SetCachedText(FText::Format(GetNodeTitleFormat(), Args), this); 351 | } 352 | return CachedNodeTitle; 353 | } 354 | 355 | return Super::GetNodeTitle(TitleType); 356 | } 357 | 358 | FName UK2Node_CowCreateWidgetAsync::GetCornerIcon() const 359 | { 360 | return TEXT("Graph.Latent.LatentIcon"); 361 | } 362 | 363 | bool UK2Node_CowCreateWidgetAsync::IsCompatibleWithGraph(const UEdGraph* TargetGraph) const 364 | { 365 | bool bIsCompatible = false; 366 | // Can only place events in ubergraphs and macros (other code will help prevent macros with latents from ending up in functions) 367 | EGraphType GraphType = TargetGraph->GetSchema()->GetGraphType(TargetGraph); 368 | if (GraphType == EGraphType::GT_Ubergraph || GraphType == EGraphType::GT_Macro) 369 | { 370 | bIsCompatible = true; 371 | } 372 | return bIsCompatible && Super::IsCompatibleWithGraph(TargetGraph); 373 | } 374 | 375 | void UK2Node_CowCreateWidgetAsync::CreatePinsForClass(UClass* InClass, TArray* OutClassPins) 376 | { 377 | // Prevent Super::CreatePinsForClass to change return type because it will introduce hard-ref to widget class 378 | 379 | // Cache PinSubCategoryObject because it will be changed by Super and we don't want it 380 | UEdGraphPin* ResultPin = GetResultPin(); 381 | auto CachedPinSubCategoryObject = ResultPin->PinType.PinSubCategoryObject; 382 | 383 | Super::CreatePinsForClass(InClass, OutClassPins); 384 | 385 | ResultPin->PinType.PinSubCategoryObject = CachedPinSubCategoryObject; 386 | } 387 | 388 | void UK2Node_CowCreateWidgetAsync::PinDefaultValueChanged(UEdGraphPin* ChangedPin) 389 | { 390 | if (ChangedPin && (ChangedPin->PinName == SoftWidgetClass)) 391 | { 392 | OnSoftWidgetClassChanged(); 393 | } 394 | 395 | Super::PinDefaultValueChanged(ChangedPin); 396 | } 397 | 398 | void UK2Node_CowCreateWidgetAsync::PinConnectionListChanged(UEdGraphPin* ChangedPin) 399 | { 400 | if (ChangedPin) 401 | { 402 | if (ChangedPin->PinName == SoftWidgetClass) 403 | { 404 | OnSoftWidgetClassChanged(); 405 | } else if (ChangedPin == GetResultPin()) 406 | { 407 | // If we connected Result (Return) pin to anything and it's the only connection 408 | // We want to make "proper" auto-wire 409 | // Which will connect node's OnWidgetCreated with the node we hooked Result in. 410 | // 411 | // Also see UK2Node_CowCreateWidgetAsync::IsConnectionDisallowed for details how we prevent newly placed node from auto-wiring 412 | 413 | UEdGraphPin* OnWidgetCreated = FindPinChecked(WidgetCreated, EGPD_Output); 414 | 415 | const bool bResultHasSingleConnection = GetResultPin()->LinkedTo.Num() == 1; 416 | const bool bOnWidgetCreatedPinIsntConnected = OnWidgetCreated->LinkedTo.IsEmpty(); 417 | if (bOnWidgetCreatedPinIsntConnected && bResultHasSingleConnection) 418 | { 419 | const UEdGraphNode* ResultConnectedNode = GetResultPin()->LinkedTo[0]->GetOwningNode(); 420 | UEdGraphPin* FirstAvailableExec = nullptr; 421 | const bool bHasConnectedExecs = IsAnyInputExecPinsConnected(ResultConnectedNode->Pins, FirstAvailableExec); 422 | 423 | // If the node doesn't have anything connected to execs and has available spot -> connect to it 424 | if (!bHasConnectedExecs && FirstAvailableExec) 425 | { 426 | const UEdGraphSchema_K2* K2Schema = CastChecked(GetSchema()); 427 | K2Schema->TryCreateConnection(OnWidgetCreated, FirstAvailableExec); 428 | } 429 | } 430 | } 431 | } 432 | 433 | Super::PinConnectionListChanged(ChangedPin); 434 | } 435 | 436 | void UK2Node_CowCreateWidgetAsync::OnSoftWidgetClassChanged() 437 | { 438 | UEdGraphPin* WidgetClassPin = FindPin(WidgetClass, EGPD_Input); 439 | 440 | /** 441 | * When we change (or just check) selected WidgetClass we need to do several things: 442 | * 1. Unbind from previous Blueprint's OnChange event 443 | * 2. Bind to a new Blueprint's OnChange event (to update exposed pins when they added) 444 | * 3. Update Super::WidgetClassPin for Super to correctly populate exposed pins 445 | */ 446 | UClass* OldWidgetClass = WidgetClassToSpawn; 447 | 448 | UClass* NewWidgetClass = GetClassToSpawn(); 449 | 450 | // If we happen to start the editor but our node has Serialized WidgetClassToSpawn we need to bind to owning OnChanged 451 | const bool bBindedToOnChanged = OnWidgetBlueprintChangedHandle.IsValid(); 452 | const bool bRunForOnChangedBinding = NewWidgetClass && !bBindedToOnChanged; 453 | if (bRunForOnChangedBinding || NewWidgetClass != OldWidgetClass) 454 | { 455 | // 1. Unbind from previous Blueprint's OnChange event 456 | UnbindFromBlueprintChange(); 457 | 458 | // 2. Bind to a new Blueprint's OnChange event (to update exposed pins when they added) 459 | if (NewWidgetClass) 460 | { 461 | if (auto BlueprintClass = Cast(NewWidgetClass)) 462 | { 463 | if (auto Blueprint = Cast(BlueprintClass->ClassGeneratedBy)) 464 | { 465 | WeakWidgetClassBlueprint = Blueprint; 466 | OnWidgetBlueprintChangedHandle = Blueprint->OnChanged().AddWeakLambda(this, [this](class UBlueprint*) 467 | { 468 | OnSoftWidgetClassChanged(); 469 | }); 470 | } 471 | } 472 | } 473 | 474 | WidgetClassToSpawn = NewWidgetClass; 475 | } 476 | 477 | // Fix our return type 478 | // 479 | // If soft widget class pin connected to anything that means that our type is propagated 480 | // And it's safe to use it (because hard-ref to the type is already created by the connection) 481 | // We're not the one to blame in such a situation :) 482 | UEdGraphPin* ResultPin = GetResultPin(); 483 | if (IsSoftWidgetClassConnected()) 484 | { 485 | ResultPin->PinType.PinSubCategoryObject = WidgetClassToSpawn; 486 | } else 487 | { 488 | // To not introduce hard-ref we need to iterate over Super classes until we find a most derived C++ base 489 | 490 | // Example 491 | // 492 | // UProjectWidget : UUserWidget 493 | // WBP_Base : UProjectWidget 494 | // WBP_Derived : WBP_Base 495 | // 496 | // If user selected to spawn WBP_Derived we need to set return to be UProjectWidget 497 | ResultPin->PinType.PinSubCategoryObject = GetFirstNativeClass(WidgetClassToSpawn); 498 | } 499 | 500 | // 3. Update Super::WidgetClassPin for Super to correctly populate exposed pins 501 | WidgetClassPin->DefaultObject = WidgetClassToSpawn; 502 | 503 | // Always try to regenerate 504 | OnClassPinChanged(); 505 | 506 | // Reset Parent Class pin to avoid hard-ref 507 | WidgetClassPin->DefaultObject = nullptr; 508 | 509 | // OnClassPinChanged removes OnWidgetCreatedPin 510 | TryCreateOnWidgetCreatedPin(); 511 | } 512 | 513 | void UK2Node_CowCreateWidgetAsync::UnbindFromBlueprintChange() 514 | { 515 | if (auto Blueprint = WeakWidgetClassBlueprint.Get()) 516 | { 517 | Blueprint->OnChanged().Remove(OnWidgetBlueprintChangedHandle); 518 | OnWidgetBlueprintChangedHandle.Reset(); 519 | WeakWidgetClassBlueprint.Reset(); 520 | } 521 | } 522 | 523 | bool UK2Node_CowCreateWidgetAsync::IsSpawnVarPin(UEdGraphPin* Pin) const 524 | { 525 | return( Super::IsSpawnVarPin(Pin) && 526 | Pin->PinName != SoftWidgetClass && 527 | Pin->PinName != WidgetCreated); 528 | } 529 | 530 | bool UK2Node_CowCreateWidgetAsync::IsConnectionDisallowed(const UEdGraphPin* MyPin, const UEdGraphPin* OtherPin, FString& OutReason) const 531 | { 532 | // Fix auto-wiring uses Then instead of OnWidgetCreated when dragging and using Result (UUserWidget object) 533 | 534 | // Idea is to detect when newly placed node does Autowire and tries to connect our "Then" pin to the node's "Exec" 535 | if (MyPin == GetThenPin()) 536 | { 537 | // Return is connected 538 | if (const bool bResultConnected = GetResultPin()->LinkedTo.Num() == 1) 539 | { 540 | // We need to make sure we're currently being called to check possibility of connection between the node to which our return is connected 541 | UEdGraphNode* ReturnLinkedNode = GetResultPin()->LinkedTo[0]->GetOwningNode(); 542 | if (const bool bReturnLinkedToOtherPinNode = ReturnLinkedNode == OtherPin->GetOwningNode()) 543 | { 544 | UEdGraphPin* NotUsed = nullptr; 545 | // We disallow connection in a case when node's execs aren't connected which means that we are in that exact moment trying to do auto-wire 546 | // And we want to prevent it for UK2Node_CowCreateWidgetAsync::PinConnectionListChanged to do the correct auto-wire we want 547 | // It's alright if you don't understand it, I struggled 10 minutes to write the comment of that single line... 548 | return IsAnyInputExecPinsConnected(ReturnLinkedNode->Pins, NotUsed); 549 | } 550 | } 551 | 552 | } 553 | 554 | return Super::IsConnectionDisallowed(MyPin, OtherPin, OutReason); 555 | } 556 | 557 | bool UK2Node_CowCreateWidgetAsync::IsAnyInputExecPinsConnected(const TArray& Pins, UEdGraphPin*& OutFirstUnconnectedPin) 558 | { 559 | OutFirstUnconnectedPin = nullptr; 560 | 561 | for (UEdGraphPin* Pin : Pins) 562 | { 563 | if (Pin && Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec && Pin->Direction == EEdGraphPinDirection::EGPD_Input) 564 | { 565 | if (!Pin->LinkedTo.IsEmpty()) 566 | { 567 | return true; 568 | } 569 | if (OutFirstUnconnectedPin == nullptr) 570 | { 571 | OutFirstUnconnectedPin = Pin; 572 | } 573 | } 574 | } 575 | 576 | return false; 577 | } 578 | 579 | UClass* UK2Node_CowCreateWidgetAsync::GetClassToSpawn() const 580 | { 581 | UEdGraphPin* SoftWidgetClassPin = FindPinChecked(SoftWidgetClass, EGPD_Input); 582 | 583 | // If SoftWidgetClassPin isn't connected to anything and not empty we should use Path written in DefaultValue 584 | if (!SoftWidgetClassPin->DefaultValue.IsEmpty() && SoftWidgetClassPin->LinkedTo.Num() == 0) 585 | { 586 | FSoftObjectPath SoftWidget = SoftWidgetClassPin->DefaultValue; 587 | return Cast(SoftWidget.TryLoad()); 588 | } 589 | else if (SoftWidgetClassPin->LinkedTo.Num()) 590 | { 591 | UEdGraphPin* ClassSource = SoftWidgetClassPin->LinkedTo[0]; 592 | return Cast(ClassSource->PinType.PinSubCategoryObject.Get()); 593 | } 594 | 595 | return nullptr; 596 | } 597 | 598 | UEdGraphPin* UK2Node_CowCreateWidgetAsync::GenerateConvertToSoftObjectRef(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph, UEdGraphPin* SoftClassPin) 599 | { 600 | if (!ensureAlways(SoftClassPin && SoftClassPin->PinType.PinCategory == UEdGraphSchema_K2::PC_SoftClass)) 601 | { 602 | return nullptr; 603 | } 604 | 605 | // 606 | // SoftClassPin -> Conv_SoftObjRefToSoftClassPath -> Conv_SoftObjPathToSoftObjRef (And return "return" pin of last conv) 607 | // 608 | 609 | const UEdGraphSchema_K2* Schema = CompilerContext.GetSchema(); 610 | 611 | // FSoftClassPath UKismetSystemLibrary::Conv_SoftObjRefToSoftClassPath(TSoftClassPtr SoftClassReference) 612 | UK2Node_CallFunction* SoftObjRefToSoftClassPath = CompilerContext.SpawnIntermediateNode(this, SourceGraph); 613 | SoftObjRefToSoftClassPath->SetFromFunction(UKismetSystemLibrary::StaticClass()->FindFunctionByName(GET_FUNCTION_NAME_CHECKED(UKismetSystemLibrary, Conv_SoftObjRefToSoftClassPath))); 614 | SoftObjRefToSoftClassPath->AllocateDefaultPins(); 615 | 616 | UEdGraphPin* SoftToPath_Input = SoftObjRefToSoftClassPath->FindPinChecked(TEXT("SoftClassReference"), EGPD_Input); 617 | UEdGraphPin* SoftToPath_ReturnValue = SoftObjRefToSoftClassPath->GetReturnValuePin(); 618 | 619 | CompilerContext.MovePinLinksToIntermediate(*SoftClassPin, *SoftToPath_Input); 620 | 621 | // TSoftObjectPtr UKismetSystemLibrary::Conv_SoftObjPathToSoftObjRef(const FSoftObjectPath& SoftObjectPath) 622 | UK2Node_CallFunction* SoftObjPathToSoftObjRef = CompilerContext.SpawnIntermediateNode(this, SourceGraph); 623 | SoftObjPathToSoftObjRef->SetFromFunction(UKismetSystemLibrary::StaticClass()->FindFunctionByName(GET_FUNCTION_NAME_CHECKED(UKismetSystemLibrary, Conv_SoftObjPathToSoftObjRef))); 624 | SoftObjPathToSoftObjRef->AllocateDefaultPins(); 625 | 626 | UEdGraphPin* PathToRef_Input = SoftObjPathToSoftObjRef->FindPinChecked(TEXT("SoftObjectPath"), EGPD_Input); 627 | ensureAlways(Schema->TryCreateConnection(SoftToPath_ReturnValue, PathToRef_Input)); 628 | 629 | return SoftObjPathToSoftObjRef->GetReturnValuePin(); 630 | } 631 | 632 | bool UK2Node_CowCreateWidgetAsync::IsSoftWidgetClassConnected() const 633 | { 634 | UEdGraphPin* SoftWidgetClassPin = FindPinChecked(SoftWidgetClass, EGPD_Input); 635 | return !SoftWidgetClassPin->LinkedTo.IsEmpty(); 636 | } 637 | 638 | UClass* UK2Node_CowCreateWidgetAsync::GetFirstNativeClass(UClass* Child) 639 | { 640 | UClass* Result = Child; 641 | while (Result && !Result->HasAnyClassFlags(CLASS_Native)) 642 | { 643 | Result = Result->GetSuperClass(); 644 | } 645 | return Result; 646 | } 647 | 648 | #undef LOCTEXT_NAMESPACE -------------------------------------------------------------------------------- /Source/CowNodes/Public/CowCompilerUtilities.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Oleksandr "sleepCOW" Ozerov. All rights reserved. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | 7 | class FKismetCompilerContext; 8 | 9 | namespace FCowCompilerUtilities 10 | { 11 | COWNODES_API const UClass* GetFirstNativeClass(const UClass* Child); 12 | COWNODES_API UClass* GetFirstNativeClass(UClass* Child); 13 | 14 | /** 15 | * Copy-paste of 5.5.3 FKismetCompilerUtilities::GenerateAssignmentNodes 16 | * 17 | * Changes: 18 | * 1. Fixed hard-ref introduced when generating assignments for BlueprintSetter 19 | */ 20 | COWNODES_API UEdGraphPin* GenerateAssignmentNodes(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph, UK2Node* CallBeginSpawnNode, UEdGraphNode* SpawnNode, UEdGraphPin* CallBeginResult, const UClass* ForClass, const UEdGraphPin* CallBeginClassInput = nullptr); 21 | } -------------------------------------------------------------------------------- /Source/CowNodes/Public/K2Node_CowCreateWidgetAsync.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2025 Oleksandr "sleepCOW" Ozerov. All rights reserved. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Editor/UMGEditor/Private/Nodes/K2Node_CreateWidget.h" 7 | #include "K2Node_CowCreateWidgetAsync.generated.h" 8 | 9 | /** 10 | * Improved version of Epic's CreateWidget and CreateWidgetAsync 11 | * 12 | * Combining best of both words with an attempt for general QoL improvements: 13 | * - Doesn't introduce hard-ref to the selected widget class (similar to CreateWidgetAsync) 14 | * - Shows any ExposedOnSpawn parameters (similar to CreateWidget) 15 | * - Automatically adds new ExposedOnSpawn variables (CreateWidget doesn't handle it) 16 | * - Widget class pin supports linked properties and propagates their class: 17 | * Let's say you created a soft class property MyClass of type WBP_BaseWidget with exposed member Text (so only WBP_BaseWidget and derived classes can be selected) 18 | * If you connect MyClass property to the node it will properly do: 19 | * 1. Display any exposed members, in our case Text will be shown 20 | * 2. Make a return type of the node to be WBP_BaseWidget 21 | * Because you already have hard-ref due to pin type (Yep, SoftClass property to a type X is a hard-ref to the X) 22 | * - Return value is always the best possible option that won't cause a hard-ref 23 | * either a first native class or widget class if linked to a pin which introduced hard-ref 24 | * - Proper auto-wiring for ReturnType to avoid common mistake of using "Then" pin instead of "OnWidgetCompleted" 25 | * - Generates "user-friendly" notes when you try to make something that could be an error 26 | * (e.g. introduction of hard-ref, keeping default values of base class when you hooked pin of a base class and it may differ at runtime) 27 | * - Fixed FKismetCompilerUtilities::GenerateAssignmentNodes create hard-ref through DynamicCast for native properties with BlueprintSetter 28 | * See FCowCompilerUtilities::GenerateAssignmentNodes for implementation details 29 | * 30 | * For implementation details see ExpandNode (but shortly it replaces the node with LoadAsset -> Cast to UUserWidget -> Set var calls) 31 | * 32 | * @note: Known limitations: 33 | * 1. Works only with EventGraph/Macro (because async) 34 | * 2. No support for conflicting names e.g. ExposedVar named "SoftWidgetClass" will cause error for node compilation 35 | * 3. Even if soft-ref already loaded you will have 1 frame delay because of LoadAsset (#TODO) 36 | * 4. In the editor the node holds hard-ref to the WidgetClass (This is required for pin generation and proper reloading when WidgetClass changes) 37 | */ 38 | UCLASS() 39 | class COWNODES_API UK2Node_CowCreateWidgetAsync : public UK2Node_CreateWidget 40 | { 41 | GENERATED_BODY() 42 | public: 43 | 44 | // UK2Node_ConstructObjectFromClass BEGIN 45 | virtual bool IsSpawnVarPin(UEdGraphPin* Pin) const override; 46 | virtual void CreatePinsForClass(UClass* InClass, TArray* OutClassPins = nullptr) override; 47 | // UK2Node_ConstructObjectFromClass END 48 | 49 | // Life cycle BEGIN 50 | virtual void PostLoad() override; 51 | virtual void BeginDestroy() override; 52 | virtual void ReconstructNode() override; 53 | virtual void PostReconstructNode() override; 54 | // Life cycle END 55 | 56 | virtual void AllocateDefaultPins() override; 57 | virtual void PinDefaultValueChanged(UEdGraphPin* ChangedPin) override; 58 | virtual void PinConnectionListChanged(UEdGraphPin* ChangedPin) override; 59 | virtual bool IsCompatibleWithGraph(const UEdGraph* TargetGraph) const override; 60 | virtual bool IsConnectionDisallowed(const UEdGraphPin* MyPin, const UEdGraphPin* OtherPin, FString& OutReason) const override; 61 | 62 | // COMPILATION BEGIN 63 | 64 | virtual void ExpandNode(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) override; 65 | 66 | // If we happen to connect SoftWidgetClass via Link to another pin (meaning at runtime the class may be different) 67 | // Give a note for any default values that aren't changed from the base class 68 | // @note: Must be used only when SoftWidgetClass is connected to another pin 69 | void ValidateSpawnVarPins(const FKismetCompilerContext& CompilerContext) const; 70 | bool ValidateSpawnVarPinsNameConflicts(const FKismetCompilerContext& CompilerContext) const; 71 | 72 | // Generates a series of nodes to convert SoftClassPin to SoftObjectReference 73 | // that can be hooked into AsyncLoadAsset.Asset (Soft Object Reference) 74 | // @returns last pin in chain in unconnected state 75 | UEdGraphPin* GenerateConvertToSoftObjectRef(FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph, UEdGraphPin* SoftClassPin); 76 | 77 | // COMPILATION END 78 | 79 | virtual FName GetCornerIcon() const override; 80 | virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override; 81 | virtual FText GetBaseNodeTitle() const override; 82 | virtual FText GetNodeTitleFormat() const override; 83 | 84 | // Different helpers 85 | void TryCreateOnWidgetCreatedPin(); 86 | void OnSoftWidgetClassChanged(); 87 | void UnbindFromBlueprintChange(); 88 | bool IsSoftWidgetClassConnected() const; 89 | UClass* GetClassToSpawn() const; 90 | UEdGraphPin* GetSoftWidgetPin() const; 91 | FORCEINLINE TArray> GetInternalPins() const 92 | { 93 | return { FindPinChecked(WidgetClass), FindPinChecked(SoftWidgetClass) }; 94 | } 95 | FORCEINLINE static TArray> GetInternalPinNames() 96 | { 97 | return { WidgetClass, SoftWidgetClass }; 98 | } 99 | 100 | static bool IsAnyInputExecPinsConnected(const TArray& Pins, UEdGraphPin*& OutFirstUnconnectedPin); 101 | 102 | // Including possibility that Child is Native 103 | static UClass* GetFirstNativeClass(UClass* Child); 104 | 105 | #if WITH_EDITORONLY_DATA 106 | // Used only in editor time to generate pins correctly 107 | UPROPERTY() 108 | TObjectPtr WidgetClassToSpawn; 109 | 110 | TWeakObjectPtr WeakWidgetClassBlueprint; 111 | FDelegateHandle OnWidgetBlueprintChangedHandle; 112 | #endif 113 | 114 | // This node pins 115 | static inline const FName WidgetClass = TEXT("Class"); 116 | static inline const FName WidgetCreated = TEXT("WidgetCreated"); 117 | static inline const FName SoftWidgetClass = TEXT("SoftWidgetClass"); 118 | 119 | // UK2Node_LoadAsset 120 | // Load asset node pins (can't use getters because they're exposed only in 5.5) 121 | static inline const FName LoadAsset_Input = TEXT("Asset"); 122 | static inline const FName LoadAsset_Output = TEXT("Object"); 123 | 124 | // UWidgetBlueprintLibrary::Create 125 | static inline const FName Create_InputWorldContextObject = TEXT("WorldContextObject"); 126 | static inline const FName Create_InputWidgetType = TEXT("WidgetType"); 127 | static inline const FName Create_InputOwningPlayer = TEXT("OwningPlayer"); 128 | }; 129 | --------------------------------------------------------------------------------