Fixed some bugs"

This commit is contained in:
2026-04-08 19:39:39 -04:00
parent 5001be6c90
commit 9c2dcd9efb
37 changed files with 222 additions and 198 deletions

29
Docs/TASKS/BUGS.txt Normal file
View File

@@ -0,0 +1,29 @@
Your job is to find bugs in a single C++ source file and its header.
In order to do this, you will first need to understand the system
that the source file is a part of.
UE Wingman is an MCP that gives AI agents the ability to control the
Unreal Editor. Please read WingBasics.h, WingServer.h,
WingServer.cpp, WingFetcher.h, WingProperty.h, WingTypes.h, and
several handlers in the Handlers directory in order to understand the
overall structure of the system. Then, narrow in on the one source
file you have been assigned to analyze.
You must examine the source file as carefully as you can. Read each
subroutine one by one and note any errors in the logic. Don't just
think about the subroutine in isolation - also think about systemic
issues, like the unreal garbage collector.
When you are done, you are to write a short, concise markdown file
explaining the bugs you've found. It is not desirable to go into
great detail: just write 2-4 sentences about each bug. If in the
process of your research, you found bugs in other source files, you
should document those as well.
The markdown file's filename should be the same as the source file's
filename, with the extension changed to md. The source file which you
must review is:

View File

@@ -23,9 +23,6 @@ public:
UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results")) UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results"))
int32 Limit = 100; int32 Limit = 100;
UPROPERTY(EditAnywhere, meta=(Optional, Description="If true, include all types, not just BlueprintType/Blueprintable ones"))
bool Exhaustive = false;
virtual void Register() override virtual void Register() override
{ {
UWingServer::AddHandler(this, UWingServer::AddHandler(this,
@@ -55,7 +52,6 @@ public:
{ {
const UWingTypes::Info& Info = Pair.Value; const UWingTypes::Info& Info = Pair.Value;
if (!Info.Short.Contains(Query, ESearchCase::IgnoreCase)) continue; if (!Info.Short.Contains(Query, ESearchCase::IgnoreCase)) continue;
if (!Exhaustive && !UWingTypes::IsBlueprintType(Info) && !UWingTypes::IsBlueprintable(Info)) continue;
Matches.Add(&Info); Matches.Add(&Info);
} }

View File

@@ -1,62 +0,0 @@
* Command-handlers are classes that implement the WingHandler
interface. The command's json parameters automatically get
injected into the handler's properties using reflection
code. Check out a few handlers to see how this works.
* Class WingFetcher can precisely and easily retrieve objects
of all different kinds using a 'path'. Study the API
of this class, we use it everywhere. It is the best tool
when you need the caller to specify a blueprint, or a
graph, or a pin - you name it.
* When you want to scan for a list of assets, MCPAssets
is the best tool. This wraps Unreal's asset database
in a convenient interface. Please study this API.
* The only valid way to get a name for an object is using
WingUtils::FormatName(obj). We don't allow the use of
pin->GetName, or node->GetTitle, or any other name-fetching
routine. Using only WingUtils::FormatName guarantees
consistency: that means, names output by one tool can
be parsed by a different tool.
* To check whether a string matches an object's name,
there's only one valid way to do that as well: using
bool WingUtils::Identifies(Name, Obj).
* Concise output is better. The output of these handlers
will primarily be consumed by LLMs with finite context
windows. Avoid sending information the caller didn't ask
for. Don't echo the command parameters: the caller knows
what parameters he sent. Don't make things unnecessarily
verbose: "type=int varname=x" can be shortened to "int x."
Generate output in a form that *you* would want to
consume.
* You can pass the output StringBuilder directly into
WingFetcher and MCPAssets. If you do, these will automatically
generate good error messages. If either of these classes
returns 'false', you don't need to generate an error
message: it's already been done. Any other routine that
accepts MCPErrorCallback can do the same.
* It's good for handlers to do operations in batches,
where possible. SpawnNodes is better than SpawnNode.
When an LLM calls into an MCP, it often takes 15 to 30
seconds. It's *important* that ConnectPins can do batches,
because building a graph can involve connecting dozens
of pins.
* It is traditional to use UE_LOG in unreal code, but it
really doesn't work for us: you see, the LLM invoking the
MCP can't see the log messages, so what's the point?
Better to report problems via the response. Please remove
UE_LOG calls in handlers.
* When you're going to edit something, it's important to
mark things dirty. There's a very powerful tool for that:
WingUtils::PreEdit and WingUtils::PostEdit, which can also
be accessed through WingFetcher::PreEdit and PostEdit.
These routines are very careful about marking everything
dirty, so you don't have to worry about it.

View File

@@ -1,59 +0,0 @@
# Serious Issues Remaining in UEWingman Handlers
## Breaking API Changes
Several handlers switched from GUID-based node matching to
`WingUtils::Identifies()`. Since the only caller is our own MCP
bridge (which uses `FormatName` output from dump commands), this
is actually fine — but it's a one-way door.
- **ChangeStructNodeType, SetMaterialExpressionPosition,
DeleteMaterialExpression, DisconnectMaterialExpressionPin** —
node param now expects FormatName identifiers, not GUIDs
- **RemoveStructField, AddStructField** — switched from
MCPAssets (accepts bare names) to WingFetcher (requires full
paths)
- **SetPinDefaultValues** — entry struct changed from
`{Blueprint, Node, PinName, Value}` to `{Pin, Value}` where
Pin is a full fetcher path
- **SpawnNodesInGraph** — changed from `Blueprint` + `Graph`
params to single `Graph` path
## Potential Crashes
- **SearchAssets** — `Data.GetClass()` can return null with
`.Info()` (asset class not loaded). Would crash on
`FormatName`.
- **SpawnNodesInGraph** — assumes `GetOuter()` of a graph is
always a `UBlueprint`. Fails for level blueprints.
## Silent Error Handling Gaps
- **AddAnimStateToMachine, SetAnimStateAnimation,
SetAnimTransitionRule** — `FindStateMachineGraph` and
`FindStateByName` lack MCPErrorCallback. If the state machine
or state isn't found, error reporting is ad-hoc/incomplete.
## Behavioral Changes
- **SetBlendSpaceSamplePoints** — now aborts on animation
lookup failure instead of silently inserting a blank sample.
Probably better behavior, but different.
## Minor Concerns
- **SetNodePositions** — node search across all graphs could
match wrong node if names collide across graphs
- **ReplaceFunctionCallsInBlueprint** — connection survival
uses pointer comparison; could be unsafe if pins are
recreated during the replacement
- **DumpMaterialInstanceParameters** — parent chain lost class
type info (Material vs MaterialInstance)
## Design changes
- Saving assets is being done at somewhat unpredictable
points. It's not entirely clear that we *should* be
saving things every time an edit is made. It might be
better to have an explicit "Save" MCP command.

View File

@@ -3,7 +3,7 @@
#include "WingServer.h" #include "WingServer.h"
#include "Containers/Ticker.h" #include "Containers/Ticker.h"
UWingCommandlet::UWingCommandlet() UWingmanCommandlet::UWingmanCommandlet()
{ {
IsClient = false; IsClient = false;
IsEditor = true; IsEditor = true;
@@ -11,7 +11,7 @@ UWingCommandlet::UWingCommandlet()
LogToConsole = true; LogToConsole = true;
} }
int32 UWingCommandlet::Main(const FString& Params) int32 UWingmanCommandlet::Main(const FString& Params)
{ {
// The UWingServer editor subsystem starts the server automatically. // The UWingServer editor subsystem starts the server automatically.
// We just need to tick it, since FTickableEditorObject doesn't tick in commandlet mode. // We just need to tick it, since FTickableEditorObject doesn't tick in commandlet mode.

View File

@@ -0,0 +1,7 @@
# WingCommandlet Bug Report
## 1. The documented `-run=` name does not match the commandlet class name
`WingCommandlet.h` says to launch the tool with `-run=UEWingman`, but the actual class is `UWingCommandlet`. In Unreal, commandlets are resolved from the class name without the `Commandlet` suffix, so this class would normally be invoked as `-run=Wing` rather than `-run=UEWingman`. As written, the usage text points users at a commandlet name that does not correspond to this class, so the advertised entry point is wrong.
## 2. Startup failures leave the commandlet spinning forever
`Main()` assumes `UWingServer` has already started and then loops until engine exit, but `UWingServer::Initialize()` can return early on socket creation, bind, or listen failure without setting `bRunning`. In that case `UWingServer::TickServer()` becomes a no-op, yet the commandlet keeps sleeping and rechecking `IsEngineExitRequested()` forever. A failed server start should terminate the commandlet with an error instead of hanging silently.

View File

@@ -94,7 +94,13 @@ bool UWingComponent::CheckNotNative(FoundComponent FC, const TCHAR *Action, Wing
bool UWingComponent::CheckOwnedByBlueprint(FoundComponent FC, UBlueprint *BP, WingOut Errors) bool UWingComponent::CheckOwnedByBlueprint(FoundComponent FC, UBlueprint *BP, WingOut Errors)
{ {
if ((FC.SCS == nullptr) || (FC.SCS->GetSCS() != BP->SimpleConstructionScript)) if (FC.SCS == nullptr)
{
Errors.Printf(TEXT("Component %s is native, cannot be manipulated in the editor\n"),
*WingUtils::ExternalizeID(FC.Name));
return false;
}
if (FC.SCS->GetSCS() != BP->SimpleConstructionScript)
{ {
Errors.Printf(TEXT("Component %s belongs to blueprint %s, edit that blueprint instead"), Errors.Printf(TEXT("Component %s belongs to blueprint %s, edit that blueprint instead"),
*WingUtils::ExternalizeID(FC.Name), *WingUtils::FormatName(FC.SCS->GetSCS()->GetBlueprint())); *WingUtils::ExternalizeID(FC.Name), *WingUtils::FormatName(FC.SCS->GetSCS()->GetBlueprint()));
@@ -138,6 +144,11 @@ void UWingComponent::AddChildNode(UBlueprint *BP, USCS_Node *NewNode, FoundCompo
bool UWingComponent::AddComponent(UBlueprint *BP, UClass *Class, UWingComponentRef *Parent, FName Name, WingOut Errors) bool UWingComponent::AddComponent(UBlueprint *BP, UClass *Class, UWingComponentRef *Parent, FName Name, WingOut Errors)
{ {
if (BP->SimpleConstructionScript == nullptr)
{
Errors.Printf(TEXT("This blueprint cannot contain components.\n"));
return false;
}
TSet<FName> Names; TSet<FName> Names;
FBlueprintEditorUtils::GetClassVariableList(BP, Names); FBlueprintEditorUtils::GetClassVariableList(BP, Names);
if (Names.Contains(Name)) if (Names.Contains(Name))

View File

@@ -0,0 +1,10 @@
# WingComponent.cpp Bug Report
## Duplicate component lookup is ambiguous
`GetAll()` returns every SCS node from every ancestor blueprint, but `FindSCSNodeByName()` already treats component names as “nearest match wins.” If a child blueprint overrides or shadows a component name from an ancestor, `WingFetcher::Component` can see multiple refs with the same external ID and fail the lookup as ambiguous instead of resolving to the visible component.
## AddComponent crashes on blueprints without an SCS
`AddComponent()` assumes `BP->SimpleConstructionScript` is valid and calls `CreateNode()` / `AddNode()` unconditionally. That works for actor blueprints, but the handler can still be pointed at blueprints that do not own an SCS, which turns this helper into a null-dereference crash instead of a clean error.
## CheckOwnedByBlueprint dereferences a null SCS on its error path
`CheckOwnedByBlueprint()` explicitly allows the “missing component” case in its guard, but the error message then unconditionally reads `FC.SCS->GetSCS()->GetBlueprint()`. If the helper is ever reached with `FC.SCS == nullptr`, the function crashes while trying to report the problem.

View File

@@ -0,0 +1,5 @@
# WingEntities.cpp Review
I did not find a concrete bug in `WingEntities.cpp`. The file is a generated entity lookup table, and its constructor and static map initialization match the expectations in `WingTokenizer` for HTML entity escape handling.
The only notable behavior is that `GetName()` returns one deterministic alias per codepoint, which is exactly what `WingTokenizer::ExternalizeID()` needs when it emits readable escapes. The generated table also stays within the `TCHAR` range that the generator documents as a constraint.

View File

@@ -19,12 +19,12 @@ bool WingFactories::CheckNewAssetPath(const FString& Path, WingOut Errors)
Errors.Printf(TEXT("ERROR: Package path '%s' is not a valid package name\n"), *Path); Errors.Printf(TEXT("ERROR: Package path '%s' is not a valid package name\n"), *Path);
return false; return false;
} }
if (!Path.StartsWith(TEXT("/Game"))) if (!Path.StartsWith(TEXT("/Game/")))
{ {
Errors.Printf(TEXT("ERROR: Package path '%s' must start with '/Game'\n"), *Path); Errors.Printf(TEXT("ERROR: Package path '%s' must start with '/Game'\n"), *Path);
return false; return false;
} }
if (FindObject<UPackage>(nullptr, *Path)) if (FindObject<UPackage>(nullptr, *Path) || FPackageName::DoesPackageExist(Path))
{ {
Errors.Printf(TEXT("ERROR: An asset already exists at '%s'\n"), *Path); Errors.Printf(TEXT("ERROR: An asset already exists at '%s'\n"), *Path);
return false; return false;

View File

@@ -0,0 +1,10 @@
# WingFactories bugs
## 1. `/Game` validation is too loose
`CheckNewAssetPath` only checks `Path.StartsWith(TEXT("/Game"))`, so paths like `/GameX/Foo` or `/Game123/Bar` pass even though they are not actually under the `/Game` mount point. That makes the asset-path guard weaker than the surrounding validation suggests and can route requests into the wrong package namespace.
## 2. Existing-package detection only sees loaded packages
`CheckNewAssetPath` uses `FindObject<UPackage>(nullptr, *Path)` to decide whether the destination already exists. That only catches packages already loaded into memory, so an on-disk asset that is not currently loaded can slip through and then collide with the new create request.
## 3. `CreateAsset` assumes `GEditor` is always present
After a successful factory call, `CreateAsset` unconditionally does `GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()` and may open the asset editor. This is safe only in a fully initialized editor session; commandlet or other headless paths can leave `GEditor` null and crash here, unlike the guarded pattern used elsewhere in the plugin.

View File

@@ -145,7 +145,7 @@ WingFetcher& WingFetcher::Asset(const FString& PackagePath)
// If it's not a material, try to open the editor, but it's okay if we can't. // If it's not a material, try to open the editor, but it's okay if we can't.
if (UMaterial* Mat = ::Cast<UMaterial>(Obj.Get())) if (UMaterial* Mat = ::Cast<UMaterial>(Obj.Get()))
{ {
IAssetEditorInstance* Editor = WingUtils::CheckOpenEditorForAsset(OriginalAsset.Get(), Errors); IAssetEditorInstance* Editor = WingUtils::CheckOpenEditorForAsset(Obj.Get(), Errors);
if (!Editor) return SetError(); if (!Editor) return SetError();
IMaterialEditor *MatEditor = static_cast<IMaterialEditor*>(Editor); IMaterialEditor *MatEditor = static_cast<IMaterialEditor*>(Editor);
SetObj(MatEditor->GetMaterialInterface()->GetBaseMaterial()); SetObj(MatEditor->GetMaterialInterface()->GetBaseMaterial());

View File

@@ -0,0 +1,7 @@
# WingFetcher Bugs
## Material assets can be resolved to the wrong object
`WingFetcher::Asset()` special-cases `UMaterial`, but it uses `GetMaterialInterface()->GetBaseMaterial()` instead of the existing transient-copy helper in `WingUtils`. That means a fetch can bind to the original asset instead of the live editor copy when a material editor is already open, so later graph or node operations may hit the wrong object state.
## Whitespace validation is incomplete
`Walk()` rejects only literal space characters with `Path.Contains(TEXT(" "))`, but it does not reject tabs, newlines, or other whitespace. Those inputs can still reach the path parser and produce confusing downstream failures instead of the clean "paths may not contain whitespace" error the code promises.

View File

@@ -0,0 +1,7 @@
# WingGraphActions Bugs
## `Search()` treats `MaxResults == 0` as "return nothing"
`FWingGraphActions::Search()` breaks out of the loop before checking any candidate whenever `MaxResults` is zero, because it tests `Results.Num() == MaxResults` at the top of the loop. The header gives `MaxResults` a default value of `0`, so the API advertises a default that produces an empty result set instead of an unlimited search or a sensible default cap.
## Exact name lookup is not protected against duplicate action labels
`CollectActions()` and `CollectSpawners()` append every matching graph action, but `Search(..., Exact=true)` only compares the standardized display name. If the action database contains two spawners or schema actions that normalize to the same name, callers like `GraphNode_Add` will fail the "exactly one" check even though the requested node type is valid. The file never deduplicates or otherwise disambiguates those collisions.

View File

@@ -18,6 +18,7 @@ WingGraphExport::WingGraphExport(UEdGraph* InGraph, bool Locals, bool Details)
EmitLocalVariables(); EmitLocalVariables();
EmitGraph(); EmitGraph();
EmitComments(); EmitComments();
EmitDetailsSuggestion();
} }
WingGraphExport::WingGraphExport(UEdGraphNode* InNode, bool Locals, bool Details) WingGraphExport::WingGraphExport(UEdGraphNode* InNode, bool Locals, bool Details)
@@ -28,6 +29,7 @@ WingGraphExport::WingGraphExport(UEdGraphNode* InNode, bool Locals, bool Details
EmitLocalVariables(); EmitLocalVariables();
EmitGraph(); EmitGraph();
EmitComments(); EmitComments();
EmitDetailsSuggestion();
} }
//////////////////////////////////////////////////////// ////////////////////////////////////////////////////////

View File

@@ -0,0 +1,10 @@
# WingGraphExport Bugs
## `GetLinkedTo()` does not follow reroute/knot chains
The helper comment in [WingGraphExport.h](/home/jyelon/integration/Plugins/UEWingman/Source/UEWingman/Public/WingGraphExport.h#L25) says a linked knot node should be followed to the far end of the chain, but the implementation in [WingGraphExport.cpp](/home/jyelon/integration/Plugins/UEWingman/Source/UEWingman/Private/WingGraphExport.cpp#L39) only returns `LinkedTo[0]`. That means exported pin sources can name the reroute node instead of the real upstream source, which makes the dump inaccurate for graphs that use reroute nodes.
## String-like pin defaults are emitted without escaping
`FormatPinSource()` wraps `PC_String`, `PC_Name`, and `PC_Text` defaults in quotes, but it prints `Pin->DefaultValue` and `Pin->DefaultTextValue.ToString()` verbatim at [WingGraphExport.cpp](/home/jyelon/integration/Plugins/UEWingman/Source/UEWingman/Private/WingGraphExport.cpp#L109). Any embedded quotes, backslashes, or newlines will produce ambiguous output and can make the dump impossible to parse back reliably.
## Suppressed minor-property notice is never emitted
`EmitNode()` sets `SuppressedDetails = true` when it hides secondary properties, and `EmitDetailsSuggestion()` is clearly meant to warn the caller afterward. But neither constructor calls [WingGraphExport::EmitDetailsSuggestion](/home/jyelon/integration/Plugins/UEWingman/Source/UEWingman/Private/WingGraphExport.cpp#L336), so the user never sees the hint to rerun with `details=true`.

View File

@@ -0,0 +1,7 @@
# WingHacks Bugs
## `WasOpenedInDefaultsMode()` dereferences null without a guard
`WingHacks::WasOpenedInDefaultsMode(FBlueprintEditor* Editor)` immediately reads `Editor->*...` and has no null check. The current call site in `WingNotifier` happens to guard the pointer first, but this helper is public and unsafe on its own, so any future caller that passes a null editor will crash.
## The private-member accessor is tightly coupled to engine internals
`WingHacks` relies on the exact private member names and signatures of `FToolMenuEntry::Action`, `FToolMenuEntry::Command`, and `FBlueprintEditor::bWasOpenedInDefaultsMode`. That makes the code fragile across Unreal upgrades: a minor engine refactor will break compilation or silently invalidate the accessor trick, so this helper should be treated as a high-risk compatibility shim rather than a stable API.

View File

@@ -251,7 +251,7 @@ void WingManual::PrintManual(TSet<Section> Sections, const FWingHandlerConfig* H
WingOut::Stdout.Print(TEXT( WingOut::Stdout.Print(TEXT(
"\n MATERIAL EDITING:" "\n MATERIAL EDITING:"
"\n We do not expose material expressions directly. Instead, use" "\n We do not expose material expressions directly. Instead, use"
"\n Property_Dump and Property_Set on the material graph nodes to" "\n Details_Dump and Details_Set on the material graph nodes to"
"\n edit material expression properties." "\n edit material expression properties."
"\n" "\n"
)); ));
@@ -263,9 +263,9 @@ void WingManual::PrintManual(TSet<Section> Sections, const FWingHandlerConfig* H
"\n" "\n"
"\n We do not expose material expressions directly. Instead, you" "\n We do not expose material expressions directly. Instead, you"
"\n will be editing the material graph. However, if you Graph_Dump" "\n will be editing the material graph. However, if you Graph_Dump"
"\n a material graph, you will see that the nodes contain mxprop" "\n a material graph, you will see that the nodes contain"
"\n properties, which actually come from the material expressions." "\n properties which actually come from the material expressions."
"\n You can edit these using Property_Set on the node." "\n You can edit these using Details_Set on the node."
"\n" "\n"
)); ));
} }
@@ -280,7 +280,8 @@ void WingManual::PrintManual(TSet<Section> Sections, const FWingHandlerConfig* H
"\n Documentation_CreateAssets: Additional commands that create new assets" "\n Documentation_CreateAssets: Additional commands that create new assets"
"\n Blueprint_Dump: a summary of any blueprint" "\n Blueprint_Dump: a summary of any blueprint"
"\n Graph_Dump: a fairly detailed listing of any Graph" "\n Graph_Dump: a fairly detailed listing of any Graph"
"\n Property_Dump: show information on many objects" "\n Details_Dump: Dump the details panel for a given object"
"\n Details_Set: Manipulate the details panel for a given object"
"\n" "\n"
"\n You can use Documentation_Commands(Query=SomeCommand,Verbose=true)" "\n You can use Documentation_Commands(Query=SomeCommand,Verbose=true)"
"\n to get detailed help for a specific command." "\n to get detailed help for a specific command."

View File

@@ -0,0 +1,7 @@
# WingManual.cpp Bug Report
## 1. `Documentation_Commands` omits the descriptions it promises
`WingManual::Commands()` prints only the command prototype when `Verbose` is false, so the default `Documentation_Commands` output is just a name list. That conflicts with [Documentation_Commands.h](/home/jyelon/integration/Plugins/UEWingman/Source/UEWingman/Handlers/Documentation_Commands.h), which advertises "all the main commands with their descriptions," and it makes the non-verbose mode less useful than the command name implies.
## 2. The manual text advertises stale command names
The manual section in [WingManual.cpp](/home/jyelon/integration/Plugins/UEWingman/Source/UEWingman/Private/WingManual.cpp) still tells users to run `Property_Dump` and `Property_Set`, but those command names do not exist in the current handler registry. The actual property-oriented handlers in this plugin are named differently, so the manual gives users instructions that cannot be followed as written.

View File

@@ -0,0 +1,7 @@
# WingNotifier
## 1. `SendNotifications()` crashes in commandlet/headless mode
`SendNotifications()` dereferences `GEditor` unconditionally when it fetches the `UAssetEditorSubsystem` ([WingNotifier.cpp](./WingNotifier.cpp#L25)). That is unsafe in the commandlet path, because `WingCommandlet::Main()` explicitly ticks the wing server in commandlet mode ([WingCommandlet.cpp](/home/jyelon/integration/Plugins/UEWingman/Source/UEWingman/Private/WingCommandlet.cpp#L10)), and the notifier is always flushed at the end of each request ([WingServer.cpp](/home/jyelon/integration/Plugins/UEWingman/Source/UEWingman/Private/WingServer.cpp#L175)). Any mutating request in a headless run can therefore null-deref before the later `if (GEditor)` viewport guard is reached.
## 2. Graph edits are not fully propagated because node reconstruction is skipped
The notifier comments out the node-level reconstruction pass and only calls `NotifyGraphChanged()` on touched graphs ([WingNotifier.cpp](./WingNotifier.cpp#L58)). That is not enough for edits that change pin layout or node internals: the project already uses `ReconstructNode()` directly in `WingVariables.cpp` after variable changes ([WingVariables.cpp](/home/jyelon/integration/Plugins/UEWingman/Source/UEWingman/Private/WingVariables.cpp#L529)), which shows that graph-shape updates need more than a graph-wide change broadcast. As written, node edits can leave the open editor showing stale pins or other cached node state until a heavier manual refresh happens.

View File

@@ -109,10 +109,10 @@ FWingParameterEditor::InfoMetaMap FWingParameterEditor::GetMaterialParameters(UM
{ {
InfoMetaMap Result; InfoMetaMap Result;
if (!Material) return Result; if (!Material) return Result;
TMap<FMaterialParameterInfo, Metadata> Temp;
for (int I = 0; I < int(EMaterialParameterType::Num); I++) for (int I = 0; I < int(EMaterialParameterType::Num); I++)
{ {
EMaterialParameterType T = (EMaterialParameterType)I; EMaterialParameterType T = (EMaterialParameterType)I;
TMap<FMaterialParameterInfo, Metadata> Temp;
Material->GetAllParametersOfType(T, Temp); Material->GetAllParametersOfType(T, Temp);
for (const auto &KV : Temp) for (const auto &KV : Temp)
{ {

View File

@@ -0,0 +1,7 @@
# WingParameterEditor Bugs
## 1. `GetMaterialParameters()` reuses `Temp` across every parameter type
[`WingParameterEditor.cpp`](./WingParameterEditor.cpp) allocates one `TMap<FMaterialParameterInfo, Metadata> Temp;` and then passes the same map to `GetAllParametersOfType()` for every `EMaterialParameterType` without clearing it first. If that Unreal API appends to the map instead of replacing it, later iterations will reprocess stale entries under the wrong type, which can corrupt the material dump and even trip the `check` on the metadata type at line 119.
## 2. The cached `FProperty*` map is never refreshed after a reload
`GetGetterAndSetterMap()` builds a static map of raw `FProperty*` pointers once and keeps them forever. In the editor, hot reload or live coding can rebuild the generated `FWingParameterEditor` struct, which makes those cached pointers stale; after that, `AddOverride()` and `Print()` can dereference invalid property metadata.

View File

@@ -0,0 +1,7 @@
# WingProperty Bugs
## Unsigned numeric properties are not handled correctly
`GetDouble()` and `GetInt64()` read integral properties through `GetSignedIntPropertyValue()`, and `SetInt64Internal()` validates by round-tripping through the signed accessor as well. That means valid unsigned values above `INT64_MAX` are misread or rejected even though `IsUnsigned()` exists in the same file and clearly shows the code was meant to distinguish signed from unsigned numeric properties.
## Printed editability does not respect stripped mutable state
`GetDetails()` deliberately calls `StripEditable()` when a property should be treated as read-only, but `Print()` ignores the `Editable` field and recomputes editability from `CPF_EditConst` alone. As a result, exported details can show a property as editable even when `Set*()` will reject writes to it, which makes the diagnostics misleading and inconsistent with the actual setter behavior.

View File

@@ -128,6 +128,8 @@ void UWingServer::Deinitialize()
void UWingServer::Tick(float DeltaTime) void UWingServer::Tick(float DeltaTime)
{ {
if (!bRunning) return;
// Accept new connections (non-blocking) // Accept new connections (non-blocking)
AcceptNewConnections(); AcceptNewConnections();
@@ -158,11 +160,6 @@ void UWingServer::TickServer(float DeltaTime)
if (GWingServer) GWingServer->Tick(DeltaTime); if (GWingServer) GWingServer->Tick(DeltaTime);
} }
bool UWingServer::IsTickable() const
{
return bRunning;
}
TStatId UWingServer::GetStatId() const TStatId UWingServer::GetStatId() const
{ {
RETURN_QUICK_DECLARE_CYCLE_STAT(UWingServer, STATGROUP_Tickables); RETURN_QUICK_DECLARE_CYCLE_STAT(UWingServer, STATGROUP_Tickables);
@@ -479,12 +476,9 @@ void UWingServer::BuildWingHandlerRegistry()
} }
void UWingServer::OnModulesChanged(FName ModuleName, EModuleChangeReason Reason) void UWingServer::OnModulesChanged(FName ModuleName, EModuleChangeReason Reason)
{
if (Reason == EModuleChangeReason::ModuleLoaded)
{ {
BuildWingHandlerRegistry(); BuildWingHandlerRegistry();
} }
}
FWingHandlerConfig* UWingServer::FindHandler(const FString& Name) FWingHandlerConfig* UWingServer::FindHandler(const FString& Name)
{ {

View File

@@ -0,0 +1,7 @@
# WingServer Bug Report
## 1. Stale global server pointer after startup failure
`Initialize()` assigns `GWingServer = this` before the socket has actually been created, bound, and listened on. If any of those steps fail, the function returns early, and `Deinitialize()` also returns early when `bRunning` is still false, so `GWingServer` is never cleared again. That leaves the static helpers in [WingServer.h](/home/jyelon/integration/Plugins/UEWingman/Source/UEWingman/Public/WingServer.h) pointing at an object that is no longer a valid running server, which can crash later calls or make commandlet-mode ticking silently operate on a dead subsystem.
## 2. Handler registry is not invalidated on module unload
`OnModulesChanged()` rebuilds the handler registry only when `Reason == EModuleChangeReason::ModuleLoaded`, so module unloads are ignored. In an editor session with hot reload or plugin unload, `WingHandlerRegistry` can keep commands whose `UClass` objects have gone away, which means the server may advertise or dispatch stale handlers into unloaded code. The registry needs to be refreshed or cleared for unloads as well, not just loads.

View File

@@ -0,0 +1,7 @@
# WingTokenizer bugs
## Numeric escapes can overflow before they are validated
`FromHex()` and `FromDecimal()` accumulate into an `int32` and only check `Value > 0xFFFF` after each multiply/add. A long escape such as `&#999999999999999999;` can overflow the signed accumulator before the range check runs, which is undefined behavior and can let malformed data slip through. These helpers should validate the next digit before the arithmetic or use a wider unsigned accumulator.
## The identifier scanner accepts whitespace-like Unicode as raw identifier text
`TokenizeIdentifier()` treats every `Cat::Other` character as an identifier character, not just the printable whitelist. That means many Unicode separator characters that are not literally ASCII space or tab can be absorbed into names instead of being rejected or escaped, producing visually blank or ambiguous identifiers that violate the tokenizer's own whitespace model. The raw-name fallback should be narrowed so non-whitespace "other" characters are the only ones accepted.

View File

@@ -0,0 +1,7 @@
# WingToolMenu Bugs
## Missing schema-level context menu actions
`GetMenuItems(UGraphNodeContextMenuContext*, const FToolMenuContext&)` only calls `UEdGraphNode::GetNodeContextMenuActions()` and never asks the schema for its context menu entries. The schema call is still present in the file as a commented-out line, so any actions that are only exposed through `UEdGraphSchema::GetContextMenuActions()` are invisible to the MCP.
## Pin actions can be hidden by disabled node actions
When merging node and pin entries, the code records every node label in `OriginalLabels` before checking whether that node entry can execute. A pin entry with the same raw label is then dropped even if the node action is currently disabled, which can hide a usable pin action from the menu and leave the result incomplete.

View File

@@ -73,20 +73,6 @@ bool UWingTypes::IsBlueprintType(const FEdGraphPinType &Type)
return true; return true;
} }
bool UWingTypes::IsChildOf(const FEdGraphPinType &Type, UClass *Parent)
{
if (Type.IsContainer()) return false;
if ((Type.PinCategory != UEdGraphSchema_K2::PC_Object) &&
(Type.PinCategory != UEdGraphSchema_K2::PC_Interface))
return false;
UClass *Class = Cast<UClass>(Type.PinSubCategoryObject.Get());
if (!Class) return false;
return Class->IsChildOf(Parent);
}
bool UWingTypes::IsBlueprintable(const FEdGraphPinType &Type) bool UWingTypes::IsBlueprintable(const FEdGraphPinType &Type)
{ {
if (Type.IsContainer()) return false; if (Type.IsContainer()) return false;
@@ -101,23 +87,6 @@ bool UWingTypes::IsBlueprintable(const FEdGraphPinType &Type)
return FKismetEditorUtilities::CanCreateBlueprintOfClass(Class); return FKismetEditorUtilities::CanCreateBlueprintOfClass(Class);
} }
bool UWingTypes::IsBlueprintType(const Info &TypeInfo)
{
// Blocked types (delegates).
if (TypeInfo.PinCategory.IsNone()) return false;
// User-defined types are always valid blueprint variable types.
if (TypeInfo.IsUserDefined) return true;
// Primitives have no PinSubCategoryObject.
if (TypeInfo.PinSubCategoryObject.IsEmpty()) return true;
// Load and check.
UObject* Obj = LoadObject<UObject>(nullptr, *TypeInfo.PinSubCategoryObject);
if (!Obj) return false;
return IsBlueprintType(Obj);
}
bool UWingTypes::IsBlueprintable(const Info &TypeInfo) bool UWingTypes::IsBlueprintable(const Info &TypeInfo)
{ {
// Only classes can be blueprintable. // Only classes can be blueprintable.
@@ -134,6 +103,21 @@ bool UWingTypes::IsBlueprintable(const Info &TypeInfo)
return FKismetEditorUtilities::CanCreateBlueprintOfClass(Class); return FKismetEditorUtilities::CanCreateBlueprintOfClass(Class);
} }
bool UWingTypes::IsChildOf(const FEdGraphPinType &Type, UClass *Parent)
{
if (Type.IsContainer()) return false;
if ((Type.PinCategory != UEdGraphSchema_K2::PC_Object) &&
(Type.PinCategory != UEdGraphSchema_K2::PC_Interface))
return false;
UClass *Class = Cast<UClass>(Type.PinSubCategoryObject.Get());
if (!Class) return false;
return Class->IsChildOf(Parent);
}
const UClass *UWingTypes::FindNativeParent(const UClass *Obj) const UClass *UWingTypes::FindNativeParent(const UClass *Obj)
{ {
const UClass* Native = Obj; const UClass* Native = Obj;
@@ -498,7 +482,8 @@ bool UWingTypes::ParseWrapped(WingTokenizer& Tok, FName Wrapper, FEdGraphPinType
if (!ParsePlainIdentifier(Tok, OutType, Errors)) return false; if (!ParsePlainIdentifier(Tok, OutType, Errors)) return false;
if (OutType.PinCategory != UEdGraphSchema_K2::PC_Object) if (OutType.PinCategory != UEdGraphSchema_K2::PC_Object)
{ {
PrintParseError(Tok, *FString::Printf(TEXT("'%s' is not an object type"), *OutType.PinSubCategoryObject->GetName()), Errors); FString Message = FString::Printf(TEXT("'%s' is not an object type"), *OutType.PinCategory.ToString());
PrintParseError(Tok, *Message, Errors);
return false; return false;
} }
if (!ParseChar(Tok, '>', Errors)) return false; if (!ParseChar(Tok, '>', Errors)) return false;

View File

@@ -0,0 +1,7 @@
# WingTypes Bugs
## 1. Wrapped primitive types can crash the parser
`ParseWrapped()` assumes that anything inside `Soft<>`, `Class<>`, or `SoftClass<>` will resolve to an object type, but it reports the failure by dereferencing `OutType.PinSubCategoryObject` unconditionally. A wrapped primitive such as `Soft<int>` or `Class<float>` reaches the error branch with a null `PinSubCategoryObject`, so the parser crashes instead of returning a normal parse error. This is a hard failure in the type parser at [WingTypes.cpp:494-506](/home/jyelon/integration/Plugins/UEWingman/Source/UEWingman/Private/WingTypes.cpp#L494).
## 2. Delegate entries are treated as valid Blueprint types
`IsBlueprintType(const Info&)` says delegates are blocked, but the implementation only rejects `None` and then treats any entry with an empty `PinSubCategoryObject` as a valid Blueprint type. That means the reserved `Delegate` and `MCDelegate` registry entries are classified as searchable Blueprint types even though the pin-based checker explicitly rejects delegates. The mismatch makes `TypeName_Search` advertise types that cannot satisfy `Require.BlueprintType`, which is misleading for users and inconsistent with [WingTypes.cpp:104-119](/home/jyelon/integration/Plugins/UEWingman/Source/UEWingman/Private/WingTypes.cpp#L104).

View File

@@ -0,0 +1,10 @@
# WingUtils Bugs
## 1. Handler names and groups strip the wrong prefix
`GetHandlerName()` and `GetHandlerGroup()` both call `RemoveFromStart(TEXT("Wing_"))` on `UClass::GetName()`, but these handler classes are named `UWing_*`. That means the prefix is never removed, so registered command names stay prefixed with `UWing_`, and the grouping logic in `WingManual::Commands()` collapses everything under the same `UWing` group instead of the intended command families.
## 2. `ReplaceMaterialWithTransientCopy()` can crash when `GEditor` is null
This helper dereferences `GEditor` unconditionally at line 553 before checking whether an editor subsystem exists. In commandlet or other headless/editor-less execution paths, that is an immediate null-deref even though the rest of the module has safer editor checks in `CheckOpenEditorForAsset()`.
## 3. `WrapText()` destroys embedded formatting
`WrapText()` tokenizes input with `ParseIntoArrayWS()`, which collapses all whitespace, including newlines and repeated spaces. That is fine for plain prose, but it loses user formatting when the function is used on graph comments in `WingGraphExport::EmitComments()`, turning multi-line comments into a single flattened paragraph instead of preserving the original text content.

View File

@@ -0,0 +1,5 @@
# WingVariables Bugs
- `CreateBlueprint()` and `CreateGraph()` are not transactional. They add variables and pins one at a time, and if a later step fails the earlier edits remain in the blueprint or graph even though the command returns `false` ([WingVariables.cpp](./WingVariables.cpp#L605), [WingVariables.cpp](./WingVariables.cpp#L632)). That leaves the editor in a partially-modified state and makes retry behavior depend on exactly where the failure occurred.
- The local-variable paths never rebuild the function entry node after changing `LocalNode->LocalVariables` or creating locals. `CreateGraph()` adds locals but only reconstructs the input/output nodes, while `ModifyGraph()` and `RemoveGraph()` edit the same backing array without calling `LocalNode->ReconstructNode()` afterward ([WingVariables.cpp](./WingVariables.cpp#L665), [WingVariables.cpp](./WingVariables.cpp#L509), [WingVariables.cpp](./WingVariables.cpp#L736)). The entry node can therefore keep stale cached state even though the underlying local-variable data changed successfully.

View File

@@ -0,0 +1,7 @@
# WingWidgets Bugs
## Unloaded widget blueprints are not filtered like loaded classes
The constructor filters loaded `UClass` entries for `CLASS_Abstract`, `CLASS_Deprecated`, `CLASS_NewerVersionExists`, and `CLASS_Hidden`, but the asset-registry path only checks `IsAssetLoaded()` and `GeneratedClassPath`. That means unloaded widget blueprints that are abstract or otherwise non-instantiable can still be advertised by `Widget_SearchTypes`, even though `Widget_Add` will not be able to create them successfully.
## `PrintWidgetTree()` assumes every panel slot has content
The tree dump recurses over `Panel->GetSlots()` and immediately calls `PrintWidgetTree(Slot->Content, Depth + 1)` with no null guard. If a widget tree is partially constructed or contains an empty/malformed slot, that recursive call path can dereference a nulsl child instead of skipping the slot or printing a placeholder.

View File

@@ -7,14 +7,14 @@
* Commandlet that keeps the engine alive so the UEWingman editor subsystem * Commandlet that keeps the engine alive so the UEWingman editor subsystem
* can serve MCP requests without the full editor UI. * can serve MCP requests without the full editor UI.
* *
* Usage: UnrealEditor-Cmd.exe Project.uproject -run=UEWingman * Usage: UnrealEditor-Cmd.exe Project.uproject -run=Wingman
*/ */
UCLASS() UCLASS()
class UWingCommandlet : public UCommandlet class UWingmanCommandlet : public UCommandlet
{ {
GENERATED_BODY() GENERATED_BODY()
public: public:
UWingCommandlet(); UWingmanCommandlet();
virtual int32 Main(const FString& Params) override; virtual int32 Main(const FString& Params) override;
}; };

View File

@@ -30,7 +30,7 @@ public:
FWingGraphActions(UEdGraph* iGraph); FWingGraphActions(UEdGraph* iGraph);
// Choose a subset of the actions. // Choose a subset of the actions.
TArray<FWingGraphAction*> Search(const FString& Query, int32 MaxResults = 0, bool Exact=false); TArray<FWingGraphAction*> Search(const FString& Query, int32 MaxResults, bool Exact);
private: private:

View File

@@ -22,12 +22,7 @@ private:
// //
//////////////////////////////////////////////////////// ////////////////////////////////////////////////////////
// Get the pin that this pin is linked to. If the // Get the pin that this pin is linked to.
// pin is linked to multiple pins, returns the first.
// If the pin is linked to a knot node, follow the
// chain of knot nodes and find the pin at the other
// end of the chain. Returns nullptr if this pin
// is not linked to anything.
// //
static UEdGraphPin* GetLinkedTo(UEdGraphPin *Pin); static UEdGraphPin* GetLinkedTo(UEdGraphPin *Pin);

View File

@@ -38,7 +38,7 @@ public:
// FTickableEditorObject // FTickableEditorObject
virtual void Tick(float DeltaTime) override; virtual void Tick(float DeltaTime) override;
virtual bool IsTickable() const override; virtual bool IsTickable() const override { return true; }
virtual TStatId GetStatId() const override; virtual TStatId GetStatId() const override;
/** Tick entry point for commandlet mode (no FTickableEditorObject). */ /** Tick entry point for commandlet mode (no FTickableEditorObject). */
@@ -50,9 +50,6 @@ public:
/** Suggest that a manual section be printed after the handler finishes. */ /** Suggest that a manual section be printed after the handler finishes. */
static void SuggestManual(WingManual::Section Section) { GWingServer->SuggestedManualSections.Add(Section); } static void SuggestManual(WingManual::Section Section) { GWingServer->SuggestedManualSections.Add(Section); }
/** Whether the server is currently listening. */
bool IsRunning() const { return bRunning; }
/** Port the server is listening on. */ /** Port the server is listening on. */
int32 GetPort() const { return Port; } int32 GetPort() const { return Port; }

View File

@@ -54,12 +54,6 @@ public:
// True if the pintype is a child of the specified uclass. // True if the pintype is a child of the specified uclass.
static bool IsChildOf(const FEdGraphPinType &Type, UClass *Class); static bool IsChildOf(const FEdGraphPinType &Type, UClass *Class);
// True if the info struct represents a BlueprintType
static bool IsBlueprintType(const Info &TypeInfo);
// True if the info struct represents a Blueprintable type.
static bool IsBlueprintable(const Info &TypeInfo);
// Get the native parent of any uclass. // Get the native parent of any uclass.
const UClass *FindNativeParent(const UClass *Class); const UClass *FindNativeParent(const UClass *Class);