diff --git a/AGENTS.md b/AGENTS.md index e90ded11..4e027727 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,8 +13,8 @@ The luprex DLL never calls into the Unreal driver. Output goes through polled bu - Use `build.py` for all builds. Do NOT follow Epic's standard build instructions. - `build.py all` — full rebuild (engine, game, intellisense, project files) -- `build.py c++` — lightweight rebuild (only if you've only edited C++ files in this repo) -- Lua and Blueprint edits don't require a rebuild. +- `build.py c++` — lightweight rebuild (use if you've only edited c++ files) +- Lua and Blueprint edits don't require any kind of build. ## Directory Structure @@ -24,6 +24,8 @@ The luprex DLL never calls into the Unreal driver. Output goes through polled bu - `Docs/` — Documentation. When trying to understand this system, start with the markdown files in the Docs directory. - `Config/` — Unreal config files - `EnginePatches/` — Custom engine modifications +- `Plugins/UEWingman/' - An MCP that gives you control over the unreal editor. +- `../integration.UE/` - the unreal engine source tree ## Architecture: World Models and Predictive Reexecution @@ -46,11 +48,6 @@ See `Docs/Predictive-Reexecution.md` for the full explanation. Lua scripts have no access to the Unreal API whatsoever. The scripter works with plain Lua tables, animation queues of key-value tuples, and coroutines. There are no "unreal bindings." The Luprex DLL is engine-agnostic — Unreal (or any other front end) interprets the animation queues and renders accordingly. -Small concessions to reality exist in animation step tuples: -- `bp` — names which Tangible Actor blueprint to use for rendering -- `mat_color` — a persistent value that hints at material color -These are still just key-value data; the script never touches the Unreal API directly. - ## Architecture: Tangibles Tangibles are game objects. Each has: @@ -70,10 +67,6 @@ On the Unreal side, **Tangible Actor blueprints** (TangibleStaticMesh, TangibleS - **LuaDefine macro** — declares Lua-callable C++ functions and auto-registers them in a global registry for automatic insertion into the Lua environment. - **eng::malloc heap** — custom deterministic memory allocator for the driven portion, ensuring reproducible addresses during replay. -## Architecture: Determinism - -The driven portion must be fully deterministic so that synchronous models stay in lockstep and event replay works. Rules: no true random numbers, no iterating unordered maps, no real-time clocks, no threads (with carefully sandboxed exceptions). See `Docs/The-Event-Driven-Structure-of-the-Engine.md`. - ## Architecture: GUI System Blueprints call into Lua via two mechanisms: @@ -82,39 +75,22 @@ Blueprints call into Lua via two mechanisms: Look-at widgets, hotkeys, and menus are built on top of this. The menu system is implemented entirely in "user space" Lua and blueprint code. See `Docs/Displaying-Widget-Blueprints.md`. -## Blueprint Text Export - -Blueprints are automatically exported to readable text files in `Saved/BlueprintExports/` whenever they are saved in the editor. This lets Claude Code read blueprint logic. See `Docs/Blueprint Text Export.md` for format details. Source: `Source/Integration/BlueprintExporter.h/.cpp` and `Source/Integration/Integration.cpp`. - ## Key Documentation Look in the Docs directory for important documentation. -## Git - -Do not use git to make changes (commit, push, branch, etc.). Read-only git commands (status, log, diff, etc.) are fine. - ## Workflow -- When the user gives a direct command, execute it. But when proposing changes on your own initiative, describe the plan and get approval before editing files. +- Do not use git to make changes (commit, push, branch, etc.). Read-only git commands (status, log, diff, etc.) are fine. +- Work at the user's pace. Do not start coding until the user says it is time. - If an instruction ends with an ellipsis (`...`), the user has more to say. Wait for the next message before acting. +- Do not output multiple paragraphs. Doing so is very rude. You are having a conversation, give the other person a + chance to speak. For most questions, 3-4 sentences is the maximum, unless you've been asked to give a + detailed explanation. ## Coding Conventions - Prefer early returns and `continue` to reduce nesting (never-nester style). -- Do not use static functions in Unreal code. Use class methods or namespace-scoped functions instead. -- Use `LogLuprexIntegration` for log messages, not `LogTemp`. -- When writing UFUNCTIONs that take an `AActor*`, `UObject*`, or similar "self" parameter, add `DefaultToSelf` meta to that pin. Most functions should have this on the obvious pin so the user doesn't have to manually wire it in blueprints. +- Do not use static functions in Unreal code. Use class methods instead. +- Use `LogLuprexIntegration` for log messages inside Source/. Use LogTemp inside Plugins/. -## Session Startup - -At the beginning of every session, do a directory listing of these three directories so you know what files are available: -- `Docs/` — documentation -- `Source/Integration/` — Unreal-side C++ code -- `luprex/cpp/core/` — Luprex DLL core C++ code - -These two code directories contain 99% of the code we'll be working on together. - -## Current Status - -(Use this section to track what we're working on across sessions.) diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 120000 index 47dc3e3d..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -AGENTS.md \ No newline at end of file diff --git a/Docs/TODO-List.md b/Docs/TODO-List.md index 1f4566d2..c885c5aa 100644 --- a/Docs/TODO-List.md +++ b/Docs/TODO-List.md @@ -1,5 +1,7 @@ * UE Wingman Widgets: cannot edit 'Is Variable' flag or widget name. + TSharedPtr NameValidator = MakeShareable(new FKismetNameValidator(Blueprint, OldObjectName)); + * Keyboard Event Handling * Menus diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/ActorComponent_Add.h b/Plugins/UEWingman/Source/UEWingman/Handlers/ActorComponent_Add.h index 88b3ab3f..e8f75117 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/ActorComponent_Add.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/ActorComponent_Add.h @@ -51,7 +51,7 @@ public: FName InternalID = WingUtils::CheckProposedName(Component, WingOut::Stdout); if (InternalID.IsNone()) return; TSet Names; - FBlueprintEditorUtils::GetClassVariableList(BP, Names); + WingUtils::GetAllInUseNames(BP, Names); if (!WingUtils::FindNoDuplicateName(Names, InternalID, TEXT("variable or component"), WingOut::Stdout)) return; // Resolve the component class by name diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/EventDispatcher_Add.h b/Plugins/UEWingman/Source/UEWingman/Handlers/EventDispatcher_Add.h index 6172329b..2ebbe952 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/EventDispatcher_Add.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/EventDispatcher_Add.h @@ -46,7 +46,7 @@ public: FName InternalID = WingUtils::CheckProposedName(Dispatcher, WingOut::Stdout); if (InternalID.IsNone()) return; TSet Names; - FBlueprintEditorUtils::GetClassVariableList(BP, Names); + WingUtils::GetAllInUseNames(BP, Names); if (!WingUtils::FindNoDuplicateName(Names, InternalID, TEXT("variable or component"), WingOut::Stdout)) return; // Parse the arguments. diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Widget_Add.h b/Plugins/UEWingman/Source/UEWingman/Handlers/Widget_Add.h index 41e7f312..c4d9ba47 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/Widget_Add.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/Widget_Add.h @@ -51,7 +51,6 @@ public: WingFetcher F(WingOut::Stdout); UWidgetBlueprint* BP = F.Walk(Blueprint).Cast(); if (!BP) return; - UWidgetTree* Tree = BP->WidgetTree; // Resolve the widget type. WingWidgets WidgetMenu; @@ -71,15 +70,14 @@ public: if (InternalID.IsNone()) return; // Check that the name is unique among existing widgets. - TArray AllWidgets; - Tree->GetAllWidgets(AllWidgets); TSet Names; - FBlueprintEditorUtils::GetClassVariableList(BP, Names); - if (!WingUtils::FindNoDuplicateNames(Names, AllWidgets, TEXT("widget or variable"), WingOut::Stdout)) return; + WingUtils::GetAllInUseNames(BP, Names); if (!WingUtils::FindNoDuplicateName(Names, InternalID, TEXT("widget or variable"), WingOut::Stdout)) return; // If a parent is specified, find it and verify it's a panel. UPanelWidget* ParentPanel = nullptr; + TArray AllWidgets; + BP->WidgetTree->GetAllWidgets(AllWidgets); if (!Parent.IsEmpty()) { UWidget* ParentWidget = WingUtils::FindOneWithExternalID(Parent, AllWidgets, TEXT("Widget"), WingOut::Stdout); @@ -89,7 +87,7 @@ public: } else { - if (Tree->RootWidget != nullptr) + if (BP->WidgetTree->RootWidget != nullptr) { WingOut::Stdout.Printf(TEXT("ERROR: Widget tree already has a root widget. Specify a Parent.\n")); return; @@ -99,9 +97,9 @@ public: // Create the widget. UWidget* NewWidget; if (WidgetClass->IsChildOf(UUserWidget::StaticClass())) - NewWidget = CreateWidget(Tree, WidgetClass, InternalID); + NewWidget = CreateWidget(BP->WidgetTree, WidgetClass, InternalID); else - NewWidget = NewObject(Tree, WidgetClass, InternalID, RF_Transactional); + NewWidget = NewObject(BP->WidgetTree, WidgetClass, InternalID, RF_Transactional); if (!NewWidget) { @@ -120,7 +118,7 @@ public: } else { - Tree->RootWidget = NewWidget; + BP->WidgetTree->RootWidget = NewWidget; } WingOut::Stdout.Printf(TEXT("Created widget '%s' of type '%s'\n"), *Name, *Type); diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingComponent.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingComponent.cpp index a8b896b7..c6f233c3 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingComponent.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingComponent.cpp @@ -150,7 +150,7 @@ bool UWingComponent::AddComponent(UBlueprint *BP, UClass *Class, UWingComponentR return false; } TSet Names; - FBlueprintEditorUtils::GetClassVariableList(BP, Names); + WingUtils::GetAllInUseNames(BP, Names); if (Names.Contains(Name)) { Errors.Printf(TEXT("There is already a variable or component named %s in %s.\n"), diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingGraphActions.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingGraphActions.cpp index 74f1d446..4b84b650 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingGraphActions.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingGraphActions.cpp @@ -1,10 +1,75 @@ #include "WingGraphActions.h" -#include "EdGraph/EdGraphSchema.h" +#include "AnimNotifyEventNodeSpawner.h" +#include "BlueprintActionFilter.h" #include "BlueprintActionDatabase.h" +#include "BlueprintBoundEventNodeSpawner.h" +#include "BlueprintComponentNodeSpawner.h" +#include "BlueprintDelegateNodeSpawner.h" +#include "BlueprintEventNodeSpawner.h" +#include "BlueprintFunctionNodeSpawner.h" #include "BlueprintNodeSpawner.h" +#include "BlueprintVariableNodeSpawner.h" +#include "EdGraph/EdGraphSchema.h" #include "EdGraphSchema_K2.h" +#include "Animation/AnimInstance.h" +#include "Engine/Blueprint.h" +#include "Kismet2/BlueprintEditorUtils.h" #include "WingUtils.h" +FName FWingGraphAction::GetPropertyClassName(const FProperty *Prop) +{ + if (Prop == nullptr) return FName(); + UClass *Class = Prop->GetOwnerClass(); + if (Class == nullptr) return FName(); + return Class->GetFName(); +} + +FName FWingGraphAction::GetSpawnerOwnerClass(const UBlueprintNodeSpawner* Spawner) +{ + if (const UBlueprintFunctionNodeSpawner* FunctionSpawner = Cast(Spawner)) + { + return FunctionSpawner->GetFunction()->GetOwnerClass()->GetFName(); + } + + if (const UBlueprintVariableNodeSpawner* VariableSpawner = Cast(Spawner)) + { + return GetPropertyClassName(VariableSpawner->GetVarProperty()); + } + + if (const UBlueprintDelegateNodeSpawner* DelegateSpawner = Cast(Spawner)) + { + return GetPropertyClassName(DelegateSpawner->GetDelegateProperty()); + } + + if (const UBlueprintBoundEventNodeSpawner* BoundEventSpawner = Cast(Spawner)) + { + return GetPropertyClassName(BoundEventSpawner->GetEventDelegate()); + } + + if (Cast(Spawner)) + { + return UAnimInstance::StaticClass()->GetFName(); + } + + if (const UBlueprintEventNodeSpawner* EventSpawner = Cast(Spawner)) + { + if (EventSpawner->IsForCustomEvent()) return FName(); + return EventSpawner->GetEventFunction()->GetOwnerClass()->GetFName(); + } + + if (const UBlueprintComponentNodeSpawner* ComponentSpawner = Cast(Spawner)) + { + if (const UClass* ComponentClass = ComponentSpawner->GetComponentClass()) + { + return ComponentClass->GetFName(); + } + // todo: get a class name from the asset data. + return FName(); + } + + return FName(); +} + FWingGraphAction::FWingGraphAction(TSharedPtr &iAction, UEdGraph *iGraph) { Action = iAction; @@ -20,10 +85,13 @@ FWingGraphAction::FWingGraphAction(UBlueprintNodeSpawner *iSpawner, UEdGraph *iG Spawner = iSpawner; Graph = iGraph; const FBlueprintActionUiSpec& UiSpec = Spawner->PrimeDefaultUiSpec(); - FString Category = UiSpec.Category.ToString(); - FString MenuName = UiSpec.MenuName.ToString(); - Name = WingUtils::StandardizeMenuItem(Category + TEXT("|") + MenuName); - Keywords = Spawner->PrimeDefaultUiSpec().Keywords.ToString(); + Name = UiSpec.Category.ToString() + TEXT("|") + UiSpec.MenuName.ToString(); + Keywords = UiSpec.Keywords.ToString(); + if (FName OwnerClass = GetSpawnerOwnerClass(Spawner); !OwnerClass.IsNone()) + { + Name = OwnerClass.ToString() + TEXT("|") + Name; + } + Name = WingUtils::StandardizeMenuItem(Name); } UEdGraphNode* FWingGraphAction::Execute(const FVector2D &Location) const @@ -72,18 +140,28 @@ void FWingGraphActions::CollectActions() void FWingGraphActions::CollectSpawners() { + UBlueprint* Blueprint = FBlueprintEditorUtils::FindBlueprintForGraph(Graph); + if (!Blueprint) return; + FBlueprintActionContext Context; + Context.Blueprints.Add(Blueprint); + Context.Graphs.Add(Graph); + + FBlueprintActionFilter Filter(FBlueprintActionFilter::BPFILTER_NoFlags); + Filter.Context = Context; + for (const auto& Pair : FBlueprintActionDatabase::Get().GetAllActions()) { + UObject* ActionObject = Pair.Key.ResolveObjectPtr(); + if (!ActionObject) continue; + for (UBlueprintNodeSpawner* Spawner : Pair.Value) { - if (Spawner->NodeClass) - { - UEdGraphNode* NodeCDO = CastChecked(Spawner->NodeClass->ClassDefaultObject); - if (NodeCDO->IsCompatibleWithGraph(Graph)) - { - Actions.Emplace(Spawner, Graph); - } - } + if (!Spawner->NodeClass) continue; + + FBlueprintActionInfo ActionInfo(ActionObject, Spawner); + if (Filter.IsFiltered(ActionInfo)) continue; + + Actions.Emplace(Spawner, Graph); } } } diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingProperty.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingProperty.cpp index 67a855c5..a88c3f62 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingProperty.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingProperty.cpp @@ -474,7 +474,8 @@ TArray FWingProperty::GetDetails(UObject* Obj, bool Mutable) } // If it's a Widget, hide the slot property, and add the - // slot properties. + // slot properties. Then, add the 'bIsVariable' property and + // make it editable. if (UWidget* Widget = Cast(Obj)) { FWingProperty::Remove(Result, TEXT("Slot")); @@ -482,6 +483,8 @@ TArray FWingProperty::GetDetails(UObject* Obj, bool Mutable) { Result.Append(GetVisible(Slot)); } + FProperty *VarProp = Widget->GetClass()->FindPropertyByName(TEXT("bIsVariable")); + if (VarProp) Result.Add(FWingProperty(VarProp, Widget, true)); } if (!Mutable) StripEditable(Result); diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp index d3e64ad2..1886069b 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp @@ -10,6 +10,8 @@ #include "Kismet2/Kismet2NameValidators.h" #include "UObject/UObjectIterator.h" #include "UObject/UnrealType.h" +#include "WidgetBlueprint.h" +#include "Blueprint/WidgetTree.h" // Reparent validation #include "Engine/LevelScriptActor.h" @@ -62,6 +64,11 @@ FName WingUtils::CheckInternalizeID(const FString &ExternalID, WingOut Errors) FName WingUtils::CheckProposedName(const FString &ExternalID, WingOut Errors) { FName InternalID = CheckInternalizeID(ExternalID, Errors); + if (InternalID.ToString().Len() > 100) + { + Errors.Printf(TEXT("ERROR: id %s is too long"), *ExternalID); + return FName(); + } if (!InternalID.IsNone() && !WingTokenizer::WouldExternalizeReadably(InternalID)) { Errors.Printf(TEXT("ERROR: id %s would not be a readable id, may not create item with this name"), @@ -476,6 +483,21 @@ UObject *WingUtils::GetGeneratedCDO(UBlueprint *BP) return BP->GeneratedClass->GetDefaultObject(); } +void WingUtils::GetAllInUseNames(UBlueprint *BP, TSet &Names) +{ + FBlueprintEditorUtils::GetClassVariableList(BP, Names, true); + FBlueprintEditorUtils::GetFunctionNameList(BP, Names); + FBlueprintEditorUtils::GetAllGraphNames(BP, Names); + FBlueprintEditorUtils::GetSCSVariableNameList(BP, Names); + FBlueprintEditorUtils::GetImplementingBlueprintsFunctionNameList(BP, Names); + if (UWidgetBlueprint *WBP = Cast(BP)) + { + TArray AllWidgets; + WBP->WidgetTree->GetAllWidgets(AllWidgets); + WingUtils::AddNamesToSet(Names, AllWidgets); + } +} + // ============================================================ // Graph Pin Helpers // ============================================================ diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingVariables.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingVariables.cpp index bd2b5306..d56c187e 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingVariables.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingVariables.cpp @@ -612,7 +612,7 @@ bool WingVariables::CreateBlueprint(WingOut Errors) // Check for name collisions against existing variables, components, and the like. TSet Names; - FBlueprintEditorUtils::GetClassVariableList(Blueprint.Get(), Names); + WingUtils::GetAllInUseNames(Blueprint.Get(), Names); if (!WingUtils::FindNoDuplicateNames(Names, BlueprintVariables.Variables, TEXT("variable or component"), Errors)) return false; // Create the variables. diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingGraphActions.h b/Plugins/UEWingman/Source/UEWingman/Public/WingGraphActions.h index 0589d13c..f9418e85 100644 --- a/Plugins/UEWingman/Source/UEWingman/Public/WingGraphActions.h +++ b/Plugins/UEWingman/Source/UEWingman/Public/WingGraphActions.h @@ -5,6 +5,7 @@ class UBlueprintNodeSpawner; class UEdGraph; class UEdGraphNode; +struct FBlueprintActionContext; struct FEdGraphSchemaAction; @@ -20,6 +21,10 @@ struct FWingGraphAction FWingGraphAction(UBlueprintNodeSpawner *iSpawner, UEdGraph *iGraph); UEdGraphNode *Execute(const FVector2D &Location) const; + +private: + static FName GetSpawnerOwnerClass(const UBlueprintNodeSpawner* Spawner); + static FName GetPropertyClassName(const FProperty *Prop); }; diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingUtils.h b/Plugins/UEWingman/Source/UEWingman/Public/WingUtils.h index 6dadf375..5f87a321 100644 --- a/Plugins/UEWingman/Source/UEWingman/Public/WingUtils.h +++ b/Plugins/UEWingman/Source/UEWingman/Public/WingUtils.h @@ -136,6 +136,12 @@ public: return Result; } + template + static void AddNamesToSet(TSet &Collection, ArrayType &&Array) + { + for (auto &Elt : Array) Collection.Add(GetFName(Elt)); + } + static bool FindNoDuplicateName(TSet &Collection, FName InternalID, const TCHAR *Kind, WingOut Errors) { if (Collection.Contains(InternalID)) @@ -244,6 +250,7 @@ public: static TArray AllNodes(UEdGraph *Graph); static TArray GetAncestorBlueprints(UBlueprint *BP, bool OldestFirst = false); static UObject *GetGeneratedCDO(UBlueprint *BP); + static void GetAllInUseNames(UBlueprint *BP, TSet &Names); // ----- Graph pin helpers ----- static UEdGraphPin* CheckGetPin(UEdGraphNode* Node, FName PinName, WingOut Errors);