diff --git a/Content/NodeToCode_Refs/N2C_EventStyleGuide.cpp b/Content/NodeToCode_Refs/N2C_EventStyleGuide.cpp new file mode 100644 index 0000000..5660680 --- /dev/null +++ b/Content/NodeToCode_Refs/N2C_EventStyleGuide.cpp @@ -0,0 +1,30 @@ +// Example file implementation: Reference context for Node to Code, not included in compilation +// Purpose: Demonstrate splitting Blueprint events into separate C++ methods + +#include "N2C_EventStyleGuide.h" + +// Override engine event BeginPlay, call Super::BeginPlay() +void AN2C_EventStyleGuideActor::BeginPlay() +{ + Super::BeginPlay(); + + // Example logic: Trigger event dispatcher once + InternalCounter = 1; + OnSomethingHappened.Broadcast(InternalCounter); +} + +// Override engine event Tick, call Super::Tick(DeltaTime) +void AN2C_EventStyleGuideActor::Tick(float DeltaTime) +{ + Super::Tick(DeltaTime); + + // Example logic: Increment counter + InternalCounter += 1; +} + +// Custom event implementation, maintain one-to-one correspondence with Blueprint "Custom Event" +void AN2C_EventStyleGuideActor::MyCustomEvent() +{ + // Example logic: Broadcast event + OnSomethingHappened.Broadcast(InternalCounter); +} diff --git a/Content/NodeToCode_Refs/N2C_EventStyleGuide.h b/Content/NodeToCode_Refs/N2C_EventStyleGuide.h new file mode 100644 index 0000000..ec27a6c --- /dev/null +++ b/Content/NodeToCode_Refs/N2C_EventStyleGuide.h @@ -0,0 +1,34 @@ +#pragma once + +// Example file: Reference context for Node to Code, not included in compilation +// Purpose: Demonstrate to LLM the expected style of splitting events into separate C++ methods + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" + +// Event dispatcher example: Dynamic multicast delegate +// Demonstrates how to declare an event dispatcher +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSomethingHappened, int32, Count); + +// Demonstrates event splitting rules (overriding engine events + custom events) +class AN2C_EventStyleGuideActor : public AActor +{ + GENERATED_BODY() + +public: + // Override engine events, must use override and call Super:: + virtual void BeginPlay() override; + virtual void Tick(float DeltaTime) override; + + // Custom events should be generated as Blueprint-callable UFUNCTIONs, do not merge multiple events into one function + UFUNCTION(BlueprintCallable, Category="N2C|Events") + void MyCustomEvent(); + + // Demonstrates event dispatcher declaration and usage + UPROPERTY(BlueprintAssignable, Category="N2C|Events") + FOnSomethingHappened OnSomethingHappened; + +private: + // Example data only + int32 InternalCounter = 0; +}; diff --git a/Content/Prompting/CodeGen_CPP.md b/Content/Prompting/CodeGen_CPP.md index 4b70507..b9eec68 100644 --- a/Content/Prompting/CodeGen_CPP.md +++ b/Content/Prompting/CodeGen_CPP.md @@ -159,9 +159,188 @@ - "enums": Optional array. Each enum object includes: - "name": The name of the enum + - "components": Optional array. Each component object includes: + - "ComponentName": The Blueprint variable name for the component. + - "ComponentClassName": The underlying component class name (e.g. StaticMeshComponent). + - "AttachParentName": Optional name of the parent component or root this component is attached to. + - "OverriddenProperties": Array of variable-like objects describing only those properties whose defaults were changed on this component in the Blueprint. + + ### Event Structure Preservation - STRICT ### + - Treat each Blueprint Event as a distinct C++ method. Do NOT merge multiple events into a single function. + - Engine events must be generated as **overrides** on the C++ class with proper signatures and Super calls: + - BeginPlay -> `virtual void BeginPlay() override;` and call `Super::BeginPlay();` in implementation. + - Tick -> `virtual void Tick(float DeltaTime) override;` and call `Super::Tick(DeltaTime);` in implementation. + - ConstructionScript -> `virtual void OnConstruction(const FTransform& Transform) override;` and call `Super::OnConstruction(Transform);` in implementation. + - Custom events must be generated as **Delegates** (not UFUNCTIONs) using DECLARE_DYNAMIC_MULTICAST_DELEGATE or DECLARE_DYNAMIC_DELEGATE macros, declared as UPROPERTY(BlueprintAssignable) or UPROPERTY(BlueprintCallable) with 1:1 parameter mapping from the event pins. Functions are Functions, Events are Delegates - do not confuse them. + - If a Blueprint function graph clearly overrides a parent-class virtual function, use the `override` keyword on the declaration. + - Otherwise, declare Blueprint functions as native UFUNCTIONs, e.g.: `UFUNCTION(BlueprintCallable, Category="Auto") void MyBlueprintFunction(...);`. + - Event Dispatchers must be generated using DECLARE_DYNAMIC_* macros and UPROPERTY(BlueprintAssignable), and invoked with .Broadcast(...). + - The header (.h) must contain method declarations per-event. The source (.cpp) must contain separate implementations per-event. Do NOT inline all logic into a single large function. + - If multiple events exist in the provided graphs, output multiple, separate functions and their bodies, preserving names and roles. + - Graphs of type "EventGraph" should orchestrate calls to the correct per-event functions instead of absorbing them into a monolithic function. + - **Do NOT** redefine the class skeleton (UCLASS / class declaration / ctor / dtor) inside EventGraph graphs. That belongs exclusively to the ClassItSelf graph. + + ### Class Skeleton, Components, Constructor, and Destructor - STRICT ### + - For each distinct Blueprint class identified by `metadata.BlueprintClass` (e.g. `AMCMyObjectBase`), ensure that the generated code assumes a concrete Unreal C++ class exists or will be created with: + - A default constructor, used to initialize components and member variables. + - A virtual destructor when appropriate (e.g. subclasses of AActor/APawn/UObject), even if empty, to make extension safe. + - The **exclusive place** to emit the class skeleton for C++ is the graph whose `graph_type` is `ClassItSelf` for that Blueprint class: + - Treat the `ClassItSelf` translation as responsible for defining or extending the main C++ class for this Blueprint. + - Its `graphDeclaration` must contain a **full `UCLASS` + `class` declaration block** with member fields, not just free functions or free-standing UPROPERTY/UFUNCTION declarations. + - All UPROPERTY declarations for this Blueprint **must appear inside the class body** here, not as global or namespace-scope declarations. + - You MUST also project every entry in the top-level `components[]` array into this class skeleton: + - For each component in `components[]`, declare a `UPROPERTY(...)` member whose C++ type is the appropriate component pointer type (e.g. `USceneComponent*`, `UStaticMeshComponent*`, `UTextRenderComponent*`, etc.) and whose name matches the Blueprint `ComponentName`. + - These component members belong only to the `ClassItSelf` graph; do not redeclare them in other graphs. + - It must declare the class constructor and (when appropriate) virtual destructor. + - In the constructor implementation for `ClassItSelf` (in `graphImplementation`), you MUST: + - Call `CreateDefaultSubobject<...>(TEXT("ComponentName"))` for each component, assigning the result to the corresponding component member. + - Use `SetupAttachment` (or equivalent) to attach child components to their parents based on `AttachParentName` (falling back to `RootComponent` when necessary). + - Initialize any overridden component properties listed in `OverriddenProperties` using the most semantically appropriate APIs (e.g. `SetRelativeLocation`, `SetWorldScale3D`, or direct member assignment when no better API exists). + - **Do NOT** declare or initialize components (UPROPERTY members or `CreateDefaultSubobject` calls) in any non-`ClassItSelf` graphs. Component definition and construction belong exclusively to the `ClassItSelf` graph’s `.h/.cpp`. + - Minimal expected pattern (illustrative only): + - `UCLASS()` + - `class AMyBlueprintClass : public AActor` + - `{` + - `public:` + - ` UPROPERTY(...) + - int32 SomeMember;` + - ` + - UPROPERTY(...) + - ` + - AMyBlueprintClass();` + - ` virtual ~AMyBlueprintClass() override; // when appropriate` + - ` + - // Event / function declarations may be forward-declared here or in the EventGraph graph;` + - ` // keep this graph focused on class structure and lifetime.` + - `};` + - Its `graphImplementation` must contain the constructor/destructor implementations and any class-level initialization logic (e.g., component creation and setup). It must **not** contain full EventGraph bodies; those belong in the EventGraph graph. + - When the user provides reference .h/.cpp files, integrate with the existing class instead of inventing a new one: + - Add missing constructor/declaration if it does not already exist. + - Add missing destructor declaration/implementation if appropriate. + - Add new method declarations into the existing class body. + - When the user does *not* provide source files, generate a plausible full class skeleton in `graphDeclaration` for the primary Blueprint class, including: + - `UCLASS()` macro. + - Class declaration inheriting from the implied base (e.g. `AActor`). + - Constructor and destructor declarations. + - UPROPERTY members for variables and components. + - UFUNCTION declarations for all graph-based functions and events. + + ### Variable Declaration Synthesis - STRICT ### + - Infer class-scope properties from Blueprint variable usage (e.g., VariableGet/VariableSet nodes, pins marked as Target/Member, or persistent state implied by flows). + - Declare such properties in the header (.h) with appropriate Unreal specifiers. Prefer: + - UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Auto") for commonly edited Blueprint variables. + - Use exact types derived from pin `type`/`sub_type`. For UObject-derived types, use pointers (e.g., AActor*). For structs/enums, use their native types. + - For container pins (is_array/is_map/is_set), generate TArray/ TMap/ TSet with correct value/key types. + - If type information is incomplete, emit a best-effort type plus a TODO comment explaining the assumption. Do NOT omit variable declarations. + - Ensure variable names mirror the Blueprint variable names when present; otherwise produce readable camel-case names derived from node/member names. + + ### Component Synthesis from `components[]` - STRICT ### + - The root JSON may contain a `components[]` array describing Blueprint component instances whose default properties were overridden. + - For each entry in `components[]`: + - `ComponentName`: The Blueprint variable name of the component (e.g. "Root", "LockText", "HoverLight"). + - `ComponentClassName`: The underlying component class (e.g. "SceneComponent", "StaticMeshComponent", "TextRenderComponent"). + - `AttachParentName`: Optional parent component/root name. (May be empty in some engine versions.) + - `OverriddenProperties[]`: Variable-like objects describing only those properties whose defaults were changed on this component. + + #### 1. Header (.h) component members + - For every `components[i]`, declare a corresponding UPROPERTY member on the owning class: + - Map `ComponentClassName` to the appropriate Unreal type: + - "SceneComponent" or similar -> `USceneComponent*` (or a more specific subclass when obvious). + - "StaticMeshComponent" -> `UStaticMeshComponent*`. + - "TextRenderComponent" -> `UTextRenderComponent*`. + - Use the `ComponentName` as the C++ member name (e.g. `LockText`, `HoverLight`). + - Prefer: + - `UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Components")` for these subobject members. + + #### 2. Constructor (.cpp) component creation + - In the class constructor, create each component using `CreateDefaultSubobject` with the same name as `ComponentName`: + - Example pattern matching common Unreal style: + - Root (scene) component: + - `Root = CreateDefaultSubobject(TEXT("Root"));` + - `RootComponent = Root;` + - Child components: + - `LockText = CreateDefaultSubobject(TEXT("LockText"));` + - `LockText->SetupAttachment(RootComponent);` + - When `AttachParentName` is non-empty and you can resolve a matching member, attach to that component: + - `SomeComp->SetupAttachment(ParentComp); // ParentComp name inferred from AttachParentName` + - When `AttachParentName` is empty or cannot be resolved: + - Attach non-root components to `RootComponent` (or whichever root-like scene component you declared) as a safe default. + + #### 3. Applying overridden default values + - For each entry in `components[i].OverriddenProperties[]`: + - Use its `Name`, `Type`, `TypeName`, and `DefaultValue` to emit best-effort initialization code on that component instance. + - Examples: + - Text/strings on a `UTextRenderComponent`: + - `LockText->SetText(FText::FromString("Locked"));` + - Numeric/struct properties like world size, vectors, rotators, transforms: + - `LockText->SetWorldSize(30.f);` + - `HoverLight->SetRelativeLocation(FVector(...));` + - Booleans such as visibility or physics: + - `HoverLight->SetVisibility(false);` + - `Plane->SetSimulatePhysics(true);` + - IMPORTANT: + - Only assign properties that appear in `OverriddenProperties[]`. Do NOT attempt to re-emit all defaults from the engine CDO. + - Prefer using the component's native setter functions when they are obvious (`SetText`, `SetWorldSize`, `SetVisibility`, etc.). When unclear, assign directly to the property (e.g. `Component->bSomeFlag = true;`). + + ### Blueprint Variables Mapping ### + - The N2C JSON provides variables at two different scopes: + - `variables[]` on the root FN2CBlueprint: **class member variables**. + - `graphs[].LocalVariables[]` on each FN2CGraph: **function-local variables** for that specific graph. + - Treat these as follows: + - Use `variables[]` to build a single, coherent member-variable section on the generated C++ class: + - Do NOT generate separate header/source files per variable. + - Emit one unified block of UPROPERTY fields (and any required initialization) that represents all Blueprint member variables. + - Use `graphs[i].LocalVariables[]` to build the local variable declarations for that graph/function: + - Declare these locals once, near the top of the generated function body, before they are first used. + - Use `Type`/`TypeName` and container flags to choose the C++ type. + - Use `DefaultValue` as a best-effort initialization for these locals when it is safe and reasonable. + - Never implicitly use identifiers that are not declared in: + - The function parameter list, + - `graphs[i].LocalVariables[]`, or + - `variables[]` (class members). + + ### UPROPERTY Specifiers Mapping - STRICT ### + - Choose UPROPERTY specifiers based on usage semantics inferred from Blueprint: + - EditAnywhere vs EditDefaultsOnly: If variables are commonly adjusted in editor across instances, use EditAnywhere; otherwise EditDefaultsOnly. + - BlueprintReadWrite vs BlueprintReadOnly: If Blueprint both reads and writes, use BlueprintReadWrite; if read-only from BP perspective, use BlueprintReadOnly. + - VisibleAnywhere: For runtime-only fields set by code and not intended to be edited in editor. + - Transient: For ephemeral state (e.g., DoOnce gates, runtime caches) that should not be serialized. + - SaveGame: If persistence across sessions is implied (e.g., user progress/state), add SaveGame. + - Category: Use a stable default like "Auto" unless a clear category is derivable from context. + - Use meta tags when helpful (e.g., ClampMin/ClampMax for numeric ranges inferred from pins/defaults). + - Prefer forward declarations for UObjects in headers and include headers in .cpp to minimize compile dependencies. + + ### Includes and External References - STRICT ### + - At the top of `graphImplementation`, emit necessary C++ `#include` lines for engine/framework symbols you directly use when headers are known; otherwise prefer forward declarations for UCLASS types to keep compile stable. + - For referenced Blueprints or assets with known paths, also emit pseudo-include comments to preserve reference context, e.g.: + - // include: /Game/Path/To/SomeAsset.SomeAsset_C + - For unknown header locations of engine/helper functions, add a conservative comment placeholder to guide integration, e.g.: + - // include: (resolve exact header if needed) + - Keep all pseudo-include comments grouped in a single block at the top of the implementation to aid later manual resolution. + + ### Networking & Replication - STRICT ### + - Replicated Properties: + - For variables inferred to need network sync, declare with `UPROPERTY(Replicated)` or `UPROPERTY(ReplicatedUsing=OnRep_VarName)` in `.h`. + - Generate matching `void OnRep_VarName();` and implement it in `.cpp` when `ReplicatedUsing` is used. + - In the owning class constructor, ensure `bReplicates = true;` or `SetIsReplicatedByDefault(true);` as appropriate. + - Implement `void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override;` in `.h` and define in `.cpp` using `DOREPLIFETIME(ThisClass, VarName);` for each replicated property. Include `#include "Net/UnrealNetwork.h"` in `.cpp`. + - RPC Functions: + - Generate RPCs based on Blueprint semantics: + - Server calls: `UFUNCTION(Server, Reliable)` or `(Server, Unreliable)` as inferred; name them `Server_...` by convention. + - Client calls: `UFUNCTION(Client, Reliable)` / `(Client, Unreliable)`; name them `Client_...`. + - Multicast: `UFUNCTION(NetMulticast, Reliable)` / `(Unreliable)`; name them `Multicast_...`. + - Implement authority guards where relevant: + - On server-required logic use `if (!HasAuthority()) { return; }` or `if (GetLocalRole() != ROLE_Authority) { return; }`. + - For client-only code use `IsLocallyControlled()` or `IsNetMode(NM_Client)` when appropriate. + - Replication Conditions and Meta: + - When context implies conditional replication, prefer `DOREPLIFETIME_CONDITION` and note the condition in `implementationNotes` if uncertain. + - Includes and Pseudo-Includes for Networking: + - Ensure `.cpp` has `#include "Net/UnrealNetwork.h"` when any replication or DOREPLIFETIME is emitted. + - Group networking-related pseudo-includes with other pseudo-includes at the top block. + We assume that the user is only ever providing a snippet of Blueprint logic that may include one or more graphs and optionally references to structs or enums. You must convert the **Node to Code** JSON blueprint logic into corresponding function(s) in Unreal C++ code, using native Unreal Engine C++ functions, classes, and APIs wherever possible. ### Steps to Implement @@ -296,8 +475,12 @@ **Field Requirements**: - 1. **graph_name**: The name of the graph, function, struct, or enum (taken from the N2C JSON’s fields). - 2. **graph_type**: A string reflecting the type ("Function", "EventGraph", "Composite", "Macro", "Construction", "Struct", or "Enum"). + 1. **graph_name**: The name of the graph, function, struct, or enum. + - You MUST preserve the original Blueprint graph name from the input N2C JSON. + - Do NOT rename graphs in your output (e.g., do NOT change an `EventGraph` to the Blueprint class name, or invent new names). + - The `graph_name` value in the response must be exactly equal (string match) to the source graph’s name. + - For every input graph object in the top-level `graphs[]` array of the N2C JSON, you MUST emit exactly one corresponding output graph object whose `graph_name` is identical to that input graph’s `name` field. Do not merge multiple input graphs into a single output graph, and do not omit or invent graphs. + 2. **graph_type**: A string reflecting the type ("Function", "EventGraph", "Composite", "Macro", "Construction", "ClassItSelf", "Struct", or "Enum"). 3. **graph_class**: Name of the class this graph is associated with (often from "metadata.BlueprintClass"), or an empty string if not applicable. 4. **code**: An object holding three string fields: - "graphDeclaration": diff --git a/Content/README_N2C_Config.md b/Content/README_N2C_Config.md new file mode 100644 index 0000000..bdce4b9 --- /dev/null +++ b/Content/README_N2C_Config.md @@ -0,0 +1,189 @@ +# NodeToCode 配置层优化变更说明 + +本文件记录针对 NodeToCode 生成结果进行的“配置层”优化,以实现: +1) 蓝图事件拆分为独立 C++ 方法,而非汇总到单一大函数; +2) 自动生成类成员变量声明(含 UPROPERTY 修饰符); +3) 输出必要的 `#include` 与蓝图/资产的伪 include 注释。 + +## 变更内容 +- 编辑 Prompt:`Plugins/NodeToCode/Content/Prompting/CodeGen_CPP.md` + - 新增章节:`Event Structure Preservation - STRICT` + - 每个蓝图事件对应独立 C++ 方法,禁止合并为单一大函数。 + - 引擎事件以 `override` 形式生成并调用 `Super::`。 + - 自定义事件以 `UFUNCTION(BlueprintCallable)` 形式生成,参数与引脚一一对应。 + - 事件分发器使用 `DECLARE_DYNAMIC_*` 与 `UPROPERTY(BlueprintAssignable)`,通过 `.Broadcast(...)` 触发。 + - `.h` 与 `.cpp` 分别生成声明与实现。 + - 新增章节:`Variable Declaration Synthesis - STRICT` + - 基于 Blueprint 使用自动推断并在 `.h` 中生成 `UPROPERTY(...)` 成员(精确类型、容器、TODO 注释)。 + - 新增章节:`UPROPERTY Specifiers Mapping - STRICT` + - 基于使用语义选择 `EditAnywhere/VisibleAnywhere/BlueprintReadWrite/Transient/SaveGame` 等修饰符与 `meta` 标签。 + - 新增章节:`Includes and External References - STRICT` + - 在实现顶部输出必要 `#include`;为蓝图/资产输出 `// include: /Game/...` 伪包含注释;未知头文件输出占位注释。 + - 新增章节:`Networking & Replication - STRICT` + - 变量:`UPROPERTY(Replicated)` / `UPROPERTY(ReplicatedUsing=OnRep_Var)` 与 `OnRep_Var()` 生成;构造函数 `SetIsReplicatedByDefault(true)`。 + - 生命周期:声明并实现 `GetLifetimeReplicatedProps(...)`,在 `.cpp` 使用 `DOREPLIFETIME(ThisClass, Var)`;包含 `#include "Net/UnrealNetwork.h"`。 + - RPC:按语义生成 `UFUNCTION(Server|Client|NetMulticast, Reliable|Unreliable)`,并在实现中添加必要的权限校验(如 `HasAuthority()`)。 + +- 新增参考源文件(仅用于 LLM 参考上下文,不参与编译): + - `Plugins/NodeToCode/Content/NodeToCode_Refs/N2C_EventStyleGuide.h` + - `Plugins/NodeToCode/Content/NodeToCode_Refs/N2C_EventStyleGuide.cpp` + +## 使用步骤 +1. 在 Project Settings → Plugins → Node to Code 中: + - 将 `Target Language` 设为 `C++` + - 将 `Max Translation Depth`(TranslationDepth)调至 `1` 或 `2` + - 根据偏好选择 Provider/Model(建议:Anthropic Sonnet 或 OpenAI o4-mini) + - 在 `Reference Source Files` 中添加: + - `Plugins/NodeToCode/Content/NodeToCode_Refs/N2C_EventStyleGuide.h` + - `Plugins/NodeToCode/Content/NodeToCode_Refs/N2C_EventStyleGuide.cpp` + +2. 重新执行翻译,并在输出目录(默认 `Saved/NodeToCode/Translations` 或自定义目录)对比结果结构。 + +## 期望效果 +- 多个蓝图事件将被转换为多个独立的 C++ 方法(引擎事件 `BeginPlay`/`Tick` 等为 `override`,自定义事件为 `UFUNCTION`)。 +- 事件分发器将正确以动态多播代理形式生成与触发。 +- 带网络语义的变量与调用会自动输出:`Replicated/ReplicatedUsing`、`OnRep_`、`GetLifetimeReplicatedProps`(含 `DOREPLIFETIME`)、必要的 `#include "Net/UnrealNetwork.h"`,以及 Server/Client/Multicast RPC 声明与实现骨架。 +- Blueprint 成员变量会在模型输入中以集中列表的形式提供,便于在同一个 `.h`/`.cpp` 片段中统一生成变量声明,而不是为每个变量拆散成多个脚本片段。 +- 函数局部变量现在也会被包含在模型中,以确保正确的声明和默认值。 + +## Translate Entire Blueprint 行为说明 + +- 使用工具栏上的 `Translate Entire Blueprint` 时,现在会按「图」拆分为多次请求: + - 事件图、每个函数图、每个宏图分别序列化为独立的 N2C JSON。 + - 每个 JSON 单独发送给 LLM 服务,降低单次请求的上下文长度,避免触发 DeepSeek/OpenAI 等 Provider 的最大上下文限制错误。 +- 输出落盘行为调整为“同一批次集中在一个时间戳目录下”: + - 每次执行 `Translate Entire Blueprint` 会为当前 Blueprint 生成一个带时间戳的根目录(例如 `Saved/NodeToCode/Translations/MyBP_2025-11-23-16.30.00/`)。 + - 这一轮批次内的所有图(事件图/函数图/宏图)翻译结果都会写入该目录下的子文件夹中(按图名划分子目录),便于统一查看和管理。 + - C++ 生成时的文件命名规则: + - 每个图依然有自己的子目录:`<时间戳根目录>//`。 + - 但当目标语言为 C++ 且图类型为 `EventGraph` 时,会使用 Blueprint 类名(`GraphClass`/`metadata.BlueprintClass`)作为 `.h/.cpp` 文件名: + - 例如:`AMyActor.h` / `AMyActor.cpp`,其中会集中放置:类声明、UPROPERTY 成员、组件声明与构造、构造函数/析构函数等。 + - 其他图(函数图/宏图等)仍旧以图名作为文件前缀,保持原有拆分粒度不变。 + +## Blueprint 组件(Components)默认值覆盖 + +- 从现在开始,N2C JSON 顶层会额外包含一个 `components[]` 数组,用于描述当前 Blueprint 中挂载的组件实例,以及**相对于组件类默认值被修改过的属性**: + - `ComponentName`:该组件在 Blueprint 中的变量名(例如 `MyMesh`)。 + - `ComponentClassName`:组件的类名(例如 `StaticMeshComponent`)。 + - `AttachParentName`:可选,挂载到的父组件/根(例如 `RootComponent` 或某个 SceneComponent 名)。 + - `OverriddenProperties[]`:只包含那些**被蓝图修改过**的属性,结构与 `variables[]` 类似(`Name / Type / TypeName / DefaultValue` 等)。 +- 生成 C++ 时,LLM 会参考 `components[]`: + - 在构造函数中使用 `CreateDefaultSubobject` 创建对应组件,并按 `AttachParentName` 做 `SetupAttachment`; + - 对每个组件仅对 `OverriddenProperties[]` 中出现的属性生成赋值代码(例如位置、旋转、布尔开关等),不会尝试覆盖所有引擎默认值; + - 这样可以在保持上下文体积可控的前提下,尽量复现 Blueprint 中对组件默认参数的修改。 + +## C++ 生成约定补充(构造函数 / 析构函数 / override) + +- 在 CodeGen 的 Prompt 中,新增了针对 C++ 类骨架的强约束: + - 对于每个 Blueprint Class(例如 `AMCMagicCardBase`),要求 LLM 在生成 C++ 时: + - 为该类补全/生成默认构造函数,用于创建组件(`CreateDefaultSubobject`)并初始化成员变量; + - 在合适的情况下生成虚析构函数,保证继承链安全; + - 对于引擎事件(`BeginPlay` / `Tick` / `OnConstruction` 等),使用 `override` 关键字声明,并在实现中调用 `Super::Xxx(...)`; + - 对 Blueprint 自定义函数,使用 `UFUNCTION(BlueprintCallable, Category="Auto")` 等 native 方式声明,必要时在重写父类虚函数时同样加上 `override`。 +- 当用户提供已有的 .h/.cpp 文件时,LLM 会按 Prompt 要求将这些声明/实现**合并进现有类**,而不是重新定义一个重复的类。 + +## 备注 +- `Reference Source Files` 的示例文件仅用于提示 LLM 遵循风格,不会影响项目编译逻辑。 +- 如果已有你自己的事件风格示例,建议优先加入你自己的 .h/.cpp 作为参考上下文。 + +## C++ 生成增强方案与设计说明(对社区贡献摘要) + +本节用于向社区简要说明当前针对 Blueprint → C++ 的增强设计与实现状态,便于在开源仓库中讨论与演进。 + +### 1. N2C JSON 模型扩展:components[](已实现) + +- 在 `FN2CBlueprint` 顶层新增 `components[]` 数组,描述 Blueprint 中挂载的组件实例以及**相对于组件类默认值被修改过的属性**: + - `ComponentName`:组件在 Blueprint 中的变量名。 + - `ComponentClassName`:组件类名(如 `StaticMeshComponent`、`TextRenderComponent` 等)。 + - `AttachParentName`:可选,挂载到的父组件/根名称(某些版本可能为空)。 + - `OverriddenProperties[]`:仅包含相对于对应组件类 CDO 被修改过的属性,结构沿用 `variables[]`(`Name / Type / TypeName / DefaultValue` 等)。 +- 在 `FN2CNodeTranslator::CollectComponentOverrides` 中: + - 遍历 Blueprint 的 `SimpleConstructionScript`,对每个 `USCS_Node` 的 `ComponentTemplate` 与类 CDO 做属性对比。 + - 仅当某个属性相对 CDO 发生变更时,才将其写入 `OverriddenProperties[]`,从而控制上下文长度并聚焦“真正被修改过的默认值”。 + +### 2. Prompt 侧 C++ 生成约束(已实现) + +对应文件:`Content/Prompting/CodeGen_CPP.md`。 + +- **事件与函数结构(Event Structure Preservation - STRICT)**: + - 每个 Blueprint 事件转为独立 C++ 方法,禁止合并为单一大函数。 + - 引擎事件统一以 `override` 形式声明并在实现中调用 `Super::Xxx(...)`。 + - 自定义事件 / Blueprint 函数以 `UFUNCTION(BlueprintCallable, Category="Auto")` 等 native 风格声明。 + - 事件分发器使用 `DECLARE_DYNAMIC_*` + `UPROPERTY(BlueprintAssignable)`,并通过 `.Broadcast(...)` 触发。 + - `.h` 只放声明、`.cpp` 只放实现,保持 Unreal 代码风格。 + +- **类骨架 / 构造 / 析构(Class Skeleton, Constructor, and Destructor - STRICT)**: + - 对每个 Blueprint 类(`metadata.BlueprintClass`),要求生成或补全对应的 C++ 类: + - 默认构造函数:用于创建组件并初始化成员变量。 + - 适当的虚析构函数:保证继承链安全(AActor/APawn/UObject 子类)。 + - 将 `graph_type == "EventGraph"` 对应的翻译视为**该 Blueprint 类的主 C++ 类骨架承载者**: + - 其 `graphDeclaration` 必须包含: + - `UCLASS()` 宏。 + - `class : public ` 声明。 + - 成员字段,包括: + - 由顶层 `variables[]` 推断出的 UPROPERTY 成员。 + - 由 `components[]` 推断出的组件 UPROPERTY 成员。 + - 所有事件 / 函数的 UFUNCTION 声明。 + - 其 `graphImplementation` 必须包含: + - 构造函数实现(组件 `CreateDefaultSubobject`、`SetupAttachment`、成员初始化)。 + - 需要时的析构函数实现。 + - 与类级别初始化相关的其它逻辑。 + - 当用户提供参考 `.h/.cpp` 时: + - 不新建重复类,而是将上述声明/实现合并进既有类体和 `.cpp` 文件中,并补齐缺失的构造/析构/方法声明。 + +- **变量与组件映射(Variable + Component Synthesis - STRICT)**: + - 顶层 `variables[]`: + - 视为类成员变量,在 `.h` 中生成统一的 UPROPERTY 成员区块,而不是为每个变量拆开多个脚本片段。 + - `graphs[i].LocalVariables[]`: + - 视为函数局部变量,在 `graphImplementation` 中的函数体前部声明,并尽可能根据 `DefaultValue` 进行安全初始化。 + - `components[]`: + - `.h` 中声明组件 UPROPERTY 成员(`USceneComponent*` / `UStaticMeshComponent*` / `UTextRenderComponent*` 等)。 + - 构造函数中使用 `CreateDefaultSubobject` 创建组件并根据 `AttachParentName` 调用 `SetupAttachment`,必要时退化为挂到 `RootComponent`。 + - 仅对 `OverriddenProperties[]` 中出现的属性生成赋值代码,优先使用语义化 `SetXxx` 接口,其次直接属性赋值。 + +- **网络与包含(Networking & Includes)**: + - 根据语义推断自动生成: + - `UPROPERTY(Replicated)` / `UPROPERTY(ReplicatedUsing=OnRep_)`。 + - `OnRep_` 函数及实现。 + - `GetLifetimeReplicatedProps` + `DOREPLIFETIME(ThisClass, Var)`。 + - RPC:`UFUNCTION(Server|Client|NetMulticast, Reliable|Unreliable)` + 权限检查逻辑。 + - 保证 `.cpp` 包含 `#include "Net/UnrealNetwork.h"`,并在文件顶部集中输出伪 include 注释,便于后续人工补完头文件。 + +### 3. 输出目录与 C++ 文件命名(已实现) + +- **批次根目录**: + - `Translate Entire Blueprint` 时,仍按图拆分为多个请求,每个图各自生成一份 N2C JSON。 + - 同一轮请求的所有图共享一个带时间戳的根目录,例如: + - `Saved/NodeToCode/Translations/MyBP_2025-11-23-16.30.00/`。 + - 从实现层面看,会先对整个 Blueprint 调用一次 `GenerateFromBlueprint` 生成完整的 `FN2CBlueprint`(包含所有图以及合成出来的 `ClassItSelf` 图),然后按图切片构造“每图一个 Blueprint 视图”的 N2C JSON 发送给 LLM,因此 ClassItSelf 也会参与整蓝图批量翻译流程。 + +- **每个图的子目录与文件名**: + - 每个 graph 有自己的子目录:`<时间戳根目录>//`,内部存放该图的 `.h/.cpp` 以及 `_Notes.txt` 等辅助文件。 + - 实际写盘时会对 `GraphName` 做一次**轻量的文件名安全清洗**: + - 去掉首尾空格; + - 将 Windows 不支持的文件名字符(如 `< > : " / \\ | ? *` 等)替换为下划线 `_`; + - JSON 中的 `graph_name` 保持原样不变,仅文件系统路径使用清洗后的名字。 + - 当前实现中,无论图类型(EventGraph/Function/Macro 等),生成的 C++ 文件均使用**清洗后的 `GraphName`** 作为文件名前缀: + - 例如:`EventGraph/EventGraph.h`、`SomeFunction/SomeFunction.cpp`。 + - 对于 C++ 的 `ClassItSelf` 图(`graph_type == "ClassItSelf"`)且其 `graph_class` 非空时,除了按图输出 `ClassItSelf/ClassItSelf.*` 以外,还会额外生成一对以类名命名的脚本: + - `<时间戳根目录>//.h` + - `<时间戳根目录>//.cpp` + - 这两份类名脚本仅承载类骨架: + - UCLASS 与完整 class 声明; + - 所有 Blueprint 变量对应的 UPROPERTY 成员; + - 所有 Blueprint 组件(来自 `components[]`)对应的 UPROPERTY 组件指针成员; + - 构造/析构函数声明与实现; + - 在构造函数中通过 `CreateDefaultSubobject` 创建组件、根据 `AttachParentName` 调用 `SetupAttachment`、以及对 `OverriddenProperties[]` 中属性的初始化逻辑。 + - 组件的 UPROPERTY 成员与构造阶段的创建/挂接/属性初始化**只会出现在 `.h/.cpp` 这对类骨架文件中**,不会出现在 `EventGraph` 等其他按图拆分的 `.h/.cpp` 里; + - `EventGraph` 图本身只负责事件/函数的声明与实现,不再在类目录下生成额外副本,从而保持“类结构 + 组件 + 构造/析构”和“事件逻辑”在输出层面上的清晰分离。 + +### 4. override/native 风格可配置化(规划中) + +- 目前 Prompt 中对事件与函数声明风格是“写死”的: + - 引擎事件统一使用 `override`。 + - 自定义函数统一使用 `UFUNCTION(BlueprintCallable, Category="Auto")` 等 native 风格。 +- 计划在 `UN2CSettings` 中增加配置项(例如 `bUseNativeOverrideStyleForFunctions` 或 `EN2CFunctionStyle`),并在 Prompt 组装时读取: + - 开启:维持当前“强 native + override”风格。 + - 关闭:使用更简化的函数声明风格,弱化 `override`/`UFUNCTION` 约束,方便某些项目做轻量级迁移。 + +这一系列设计在不改变 NodeToCode 核心数据模型(`FN2CBlueprint`/`FN2CGraph`)的前提下,通过 Prompt 约定与落盘命名策略,将“类骨架 + 组件 + 构造/析构”聚合到以 Blueprint 类名命名的 C++ 脚本中,同时保留按图拆分的优势,便于后续社区在此基础上迭代。 diff --git a/Content/readmd.md b/Content/readmd.md new file mode 100644 index 0000000..ba1a8f2 --- /dev/null +++ b/Content/readmd.md @@ -0,0 +1,1751 @@ +{ + "version": "1.0.0", + "metadata": + { + "name": "BP_MCGameInstance", + "blueprint_type": "Normal", + "blueprint_class": "BP_MCGameInstance" + }, + "graphs": [ + { + "name": "EventGraph", + "graph_type": "EventGraph", + "nodes": [ + { + "id": "N1", + "type": "CustomEvent", + "name": "CreateServer", + "member_name": "None", + "input_pins": [], + "output_pins": [ + { + "id": "P1", + "name": "Output Delegate", + "type": "Delegate" + }, + { + "id": "P2", + "name": "", + "connected": true + } + ] + }, + { + "id": "N3", + "type": "CallFunction", + "name": "Open Level (by Name)", + "member_parent": "GameplayStatics", + "member_name": "OpenLevel", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "Level Name", + "type": "Name", + "default_value": "None", + "connected": true + }, + { + "id": "P4", + "name": "Absolute", + "type": "Boolean", + "default_value": "true" + }, + { + "id": "P5", + "name": "Options", + "type": "String", + "default_value": "Listen" + } + ], + "output_pins": [ + { + "id": "P2", + "name": "", + "connected": true + } + ] + }, + { + "id": "N5", + "type": "CallFunction", + "name": "Print String", + "member_parent": "KismetSystemLibrary", + "member_name": "PrintString", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "In String", + "type": "String", + "default_value": "Creating Game Session..." + }, + { + "id": "P4", + "name": "Print to Screen", + "type": "Boolean", + "default_value": "true" + }, + { + "id": "P5", + "name": "Print to Log", + "type": "Boolean", + "default_value": "true" + }, + { + "id": "P6", + "name": "Text Color", + "type": "Struct", + "sub_type": "LinearColor", + "default_value": "(R=0.000000,G=1.000000,B=0.007393,A=1.000000)" + }, + { + "id": "P7", + "name": "Duration", + "type": "Real", + "sub_type": "float", + "default_value": "10" + }, + { + "id": "P8", + "name": "Key", + "type": "Name", + "default_value": "None", + "is_const": true + } + ], + "output_pins": [ + { + "id": "P2", + "name": "", + "connected": true + } + ] + }, + { + "id": "N7", + "type": "CallFunction", + "name": "Print String", + "member_parent": "KismetSystemLibrary", + "member_name": "PrintString", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "In String", + "type": "String", + "default_value": "Failed to Create Game Session..." + }, + { + "id": "P4", + "name": "Print to Screen", + "type": "Boolean", + "default_value": "true" + }, + { + "id": "P5", + "name": "Print to Log", + "type": "Boolean", + "default_value": "true" + }, + { + "id": "P6", + "name": "Text Color", + "type": "Struct", + "sub_type": "LinearColor", + "default_value": "(R=0.000000,G=1.000000,B=0.007393,A=1.000000)" + }, + { + "id": "P7", + "name": "Duration", + "type": "Real", + "sub_type": "float", + "default_value": "10" + }, + { + "id": "P8", + "name": "Key", + "type": "Name", + "default_value": "None", + "is_const": true + } + ], + "output_pins": [ + { + "id": "P2", + "name": "", + "connected": true + } + ] + }, + { + "id": "N8", + "type": "CallFunction", + "name": "Get Player Controller", + "member_parent": "GameplayStatics", + "member_name": "GetPlayerController", + "pure": true, + "input_pins": [ + { + "id": "P1", + "name": "Player Index", + "type": "Integer", + "default_value": "0" + } + ], + "output_pins": [ + { + "id": "P2", + "name": "Return Value", + "type": "Object", + "sub_type": "PlayerController", + "connected": true + } + ] + }, + { + "id": "N9", + "type": "CustomEvent", + "name": "OpenMainMenu", + "member_name": "None", + "input_pins": [], + "output_pins": [ + { + "id": "P1", + "name": "Output Delegate", + "type": "Delegate" + }, + { + "id": "P2", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "Actor", + "type": "Object", + "sub_type": "Actor", + "connected": true + } + ] + }, + { + "id": "N10", + "type": "MacroInstance", + "name": "Can Execute Cosmetic Events", + "member_parent": "StandardMacros", + "member_name": "Can Execute Cosmetic Events", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + } + ], + "output_pins": [ + { + "id": "P2", + "name": "True", + "connected": true + }, + { + "id": "P3", + "name": "False" + } + ] + }, + { + "id": "N12", + "type": "CallFunction", + "name": "Get Player Controller", + "member_parent": "GameplayStatics", + "member_name": "GetPlayerController", + "pure": true, + "input_pins": [ + { + "id": "P1", + "name": "Player Index", + "type": "Integer", + "default_value": "0" + } + ], + "output_pins": [ + { + "id": "P2", + "name": "Return Value", + "type": "Object", + "sub_type": "PlayerController", + "connected": true + } + ] + }, + { + "id": "N13", + "type": "VariableSet", + "name": "bShowMouseCursor", + "member_parent": "bool", + "member_name": "bShowMouseCursor", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "Show Mouse Cursor", + "type": "Boolean", + "default_value": "true" + }, + { + "id": "P5", + "name": "Target", + "type": "Object", + "sub_type": "PlayerController", + "connected": true + } + ], + "output_pins": [ + { + "id": "P2", + "name": "", + "connected": true + }, + { + "id": "P4", + "name": "", + "type": "Boolean", + "default_value": "false" + } + ] + }, + { + "id": "N15", + "type": "AsyncAction", + "name": "Join Session", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P5", + "name": "Player Controller", + "type": "Object", + "sub_type": "PlayerController", + "connected": true + }, + { + "id": "P6", + "name": "Search Result", + "type": "Struct", + "sub_type": "BlueprintSessionResult", + "connected": true, + "is_reference": true, + "is_const": true + } + ], + "output_pins": [ + { + "id": "P2", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "On Success", + "connected": true + }, + { + "id": "P4", + "name": "On Failure", + "connected": true + } + ] + }, + { + "id": "N19", + "type": "CallFunction", + "name": "Print String", + "member_parent": "KismetSystemLibrary", + "member_name": "PrintString", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "In String", + "type": "String", + "default_value": "Joining Game Session..." + }, + { + "id": "P4", + "name": "Print to Screen", + "type": "Boolean", + "default_value": "true" + }, + { + "id": "P5", + "name": "Print to Log", + "type": "Boolean", + "default_value": "true" + }, + { + "id": "P6", + "name": "Text Color", + "type": "Struct", + "sub_type": "LinearColor", + "default_value": "(R=0.000000,G=1.000000,B=0.007393,A=1.000000)" + }, + { + "id": "P7", + "name": "Duration", + "type": "Real", + "sub_type": "float", + "default_value": "5" + }, + { + "id": "P8", + "name": "Key", + "type": "Name", + "default_value": "None", + "is_const": true + } + ], + "output_pins": [ + { + "id": "P2", + "name": "" + } + ] + }, + { + "id": "N17", + "type": "CallFunction", + "name": "Print String", + "member_parent": "KismetSystemLibrary", + "member_name": "PrintString", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "In String", + "type": "String", + "default_value": "Joined Game Session..." + }, + { + "id": "P4", + "name": "Print to Screen", + "type": "Boolean", + "default_value": "true" + }, + { + "id": "P5", + "name": "Print to Log", + "type": "Boolean", + "default_value": "true" + }, + { + "id": "P6", + "name": "Text Color", + "type": "Struct", + "sub_type": "LinearColor", + "default_value": "(R=0.000000,G=1.000000,B=0.007393,A=1.000000)" + }, + { + "id": "P7", + "name": "Duration", + "type": "Real", + "sub_type": "float", + "default_value": "5" + }, + { + "id": "P8", + "name": "Key", + "type": "Name", + "default_value": "None", + "is_const": true + } + ], + "output_pins": [ + { + "id": "P2", + "name": "", + "connected": true + } + ] + }, + { + "id": "N18", + "type": "CallFunction", + "name": "Print String", + "member_parent": "KismetSystemLibrary", + "member_name": "PrintString", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "In String", + "type": "String", + "default_value": "Failed to Join Game Session..." + }, + { + "id": "P4", + "name": "Print to Screen", + "type": "Boolean", + "default_value": "true" + }, + { + "id": "P5", + "name": "Print to Log", + "type": "Boolean", + "default_value": "true" + }, + { + "id": "P6", + "name": "Text Color", + "type": "Struct", + "sub_type": "LinearColor", + "default_value": "(R=1.000000,G=0.000000,B=0.059079,A=1.000000)" + }, + { + "id": "P7", + "name": "Duration", + "type": "Real", + "sub_type": "float", + "default_value": "5" + }, + { + "id": "P8", + "name": "Key", + "type": "Name", + "default_value": "None", + "is_const": true + } + ], + "output_pins": [ + { + "id": "P2", + "name": "", + "connected": true + } + ] + }, + { + "id": "N21", + "type": "CustomEvent", + "name": "Event_JoinGameSession", + "member_name": "None", + "input_pins": [], + "output_pins": [ + { + "id": "P1", + "name": "Output Delegate", + "type": "Delegate" + }, + { + "id": "P2", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "Join Session", + "type": "Struct", + "sub_type": "BlueprintSessionResult", + "connected": true + } + ] + }, + { + "id": "N23", + "type": "CallFunction", + "name": "Get Player Controller", + "member_parent": "GameplayStatics", + "member_name": "GetPlayerController", + "pure": true, + "input_pins": [ + { + "id": "P1", + "name": "Player Index", + "type": "Integer", + "default_value": "0" + } + ], + "output_pins": [ + { + "id": "P2", + "name": "Return Value", + "type": "Object", + "sub_type": "PlayerController", + "connected": true + } + ] + }, + { + "id": "N14", + "type": "CallFunction", + "name": "Set View Target with Blend", + "member_parent": "PlayerController", + "member_name": "SetViewTargetWithBlend", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "Target", + "type": "Object", + "sub_type": "PlayerController", + "connected": true + }, + { + "id": "P4", + "name": "New View Target", + "type": "Object", + "sub_type": "Actor", + "connected": true + }, + { + "id": "P5", + "name": "Blend Time", + "type": "Real", + "sub_type": "float", + "default_value": "0.000000" + }, + { + "id": "P6", + "name": "Blend Func", + "type": "Byte", + "sub_type": "EViewTargetBlendFunction", + "default_value": "VTBlend_Linear" + }, + { + "id": "P7", + "name": "Blend Exp", + "type": "Real", + "sub_type": "float", + "default_value": "0.000000" + }, + { + "id": "P8", + "name": "Lock Outgoing", + "type": "Boolean", + "default_value": "false" + } + ], + "output_pins": [ + { + "id": "P2", + "name": "", + "connected": true + } + ] + }, + { + "id": "N24", + "type": "CallFunction", + "name": "Set Input Mode Game And UI", + "member_parent": "WidgetBlueprintLibrary", + "member_name": "SetInputMode_GameAndUIEx", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "Player Controller", + "type": "Object", + "sub_type": "PlayerController", + "connected": true + }, + { + "id": "P4", + "name": "In Widget to Focus", + "type": "Object", + "sub_type": "Widget" + }, + { + "id": "P5", + "name": "In Mouse Lock Mode", + "type": "Byte", + "sub_type": "EMouseLockMode", + "default_value": "DoNotLock" + }, + { + "id": "P6", + "name": "Hide Cursor During Capture", + "type": "Boolean", + "default_value": "false" + }, + { + "id": "P7", + "name": "Flush Input", + "type": "Boolean", + "default_value": "false", + "is_const": true + } + ], + "output_pins": [ + { + "id": "P2", + "name": "" + } + ] + }, + { + "id": "N25", + "type": "AsyncAction", + "name": "Create Advanced Session", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P5", + "name": "Extra Settings", + "type": "Struct", + "sub_type": "SessionPropertyKeyPair", + "is_reference": true, + "is_const": true, + "is_array": true + }, + { + "id": "P6", + "name": "Player Controller", + "type": "Object", + "sub_type": "PlayerController", + "connected": true + }, + { + "id": "P7", + "name": "Public Connections", + "type": "Integer", + "default_value": "100" + }, + { + "id": "P8", + "name": "Private Connections", + "type": "Integer", + "default_value": "0" + }, + { + "id": "P9", + "name": "Use LAN", + "type": "Boolean", + "default_value": "false" + }, + { + "id": "P10", + "name": "Allow Invites", + "type": "Boolean", + "default_value": "true" + }, + { + "id": "P11", + "name": "Is Dedicated Server", + "type": "Boolean", + "default_value": "false" + }, + { + "id": "P12", + "name": "Use Presence", + "type": "Boolean", + "default_value": "true" + }, + { + "id": "P13", + "name": "Use Lobbies if Available", + "type": "Boolean", + "default_value": "true" + }, + { + "id": "P14", + "name": "Allow Join Via Presence", + "type": "Boolean", + "default_value": "true" + }, + { + "id": "P15", + "name": "Allow Join Via Presence Friends Only", + "type": "Boolean", + "default_value": "false" + }, + { + "id": "P16", + "name": "Anti Cheat Protected", + "type": "Boolean", + "default_value": "false" + }, + { + "id": "P17", + "name": "Uses Stats", + "type": "Boolean", + "default_value": "false" + }, + { + "id": "P18", + "name": "Should Advertise", + "type": "Boolean", + "default_value": "true" + }, + { + "id": "P19", + "name": "Use Lobbies Voice Chat if Available", + "type": "Boolean", + "default_value": "false" + }, + { + "id": "P20", + "name": "Start After Create", + "type": "Boolean", + "default_value": "true" + } + ], + "output_pins": [ + { + "id": "P2", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "On Success", + "connected": true + }, + { + "id": "P4", + "name": "On Failure", + "connected": true + } + ] + }, + { + "id": "N27", + "type": "AsyncAction", + "name": "Destroy Session", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P5", + "name": "Player Controller", + "type": "Object", + "sub_type": "PlayerController", + "connected": true + } + ], + "output_pins": [ + { + "id": "P2", + "name": "" + }, + { + "id": "P3", + "name": "On Success", + "connected": true + }, + { + "id": "P4", + "name": "On Failure", + "connected": true + } + ] + }, + { + "id": "N29", + "type": "CallFunction", + "name": "Get Player Controller", + "member_parent": "GameplayStatics", + "member_name": "GetPlayerController", + "pure": true, + "input_pins": [ + { + "id": "P1", + "name": "Player Index", + "type": "Integer", + "default_value": "0" + } + ], + "output_pins": [ + { + "id": "P2", + "name": "Return Value", + "type": "Object", + "sub_type": "PlayerController", + "connected": true + } + ] + }, + { + "id": "N30", + "type": "AsyncAction", + "name": "Destroy Session", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P5", + "name": "Player Controller", + "type": "Object", + "sub_type": "PlayerController", + "connected": true + } + ], + "output_pins": [ + { + "id": "P2", + "name": "" + }, + { + "id": "P3", + "name": "On Success", + "connected": true + }, + { + "id": "P4", + "name": "On Failure", + "connected": true + } + ] + }, + { + "id": "N31", + "type": "CustomEvent", + "name": "HideMainMenu", + "member_name": "None", + "input_pins": [], + "output_pins": [ + { + "id": "P1", + "name": "Output Delegate", + "type": "Delegate" + }, + { + "id": "P2", + "name": "", + "connected": true + } + ] + }, + { + "id": "N32", + "type": "MacroInstance", + "name": "Can Execute Cosmetic Events", + "member_parent": "StandardMacros", + "member_name": "Can Execute Cosmetic Events", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + } + ], + "output_pins": [ + { + "id": "P2", + "name": "True", + "connected": true + }, + { + "id": "P3", + "name": "False" + } + ] + }, + { + "id": "N34", + "type": "VariableGet", + "name": "MenuRef", + "member_parent": "object/W_StartLogo", + "member_name": "MenuRef", + "pure": true, + "input_pins": [], + "output_pins": [ + { + "id": "P1", + "name": "Menu Ref", + "type": "Object", + "sub_type": "W_StartLogo", + "connected": true + } + ] + }, + { + "id": "N33", + "type": "MacroInstance", + "name": "Is Valid", + "member_parent": "StandardMacros", + "member_name": "IsValid", + "input_pins": [ + { + "id": "P1", + "name": "Exec", + "connected": true + }, + { + "id": "P2", + "name": "Input Object", + "type": "Object", + "sub_type": "Object", + "connected": true + } + ], + "output_pins": [ + { + "id": "P3", + "name": "Is Valid", + "connected": true + }, + { + "id": "P4", + "name": "Is Not Valid" + } + ] + }, + { + "id": "N35", + "type": "CallFunction", + "name": "Remove from Parent", + "member_parent": "Widget", + "member_name": "RemoveFromParent", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "Target", + "type": "Object", + "sub_type": "Widget", + "connected": true + } + ], + "output_pins": [ + { + "id": "P2", + "name": "" + } + ] + }, + { + "id": "N36", + "type": "CustomEvent", + "name": "ShowMainMenu", + "member_name": "None", + "input_pins": [], + "output_pins": [ + { + "id": "P1", + "name": "Output Delegate", + "type": "Delegate" + }, + { + "id": "P2", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "With Anim", + "type": "Boolean", + "connected": true + } + ] + }, + { + "id": "N37", + "type": "MacroInstance", + "name": "Can Execute Cosmetic Events", + "member_parent": "StandardMacros", + "member_name": "Can Execute Cosmetic Events", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + } + ], + "output_pins": [ + { + "id": "P2", + "name": "True", + "connected": true + }, + { + "id": "P3", + "name": "False" + } + ] + }, + { + "id": "N39", + "type": "ConstructObjectFromClass", + "name": "Create Widget", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "Class", + "type": "Class", + "sub_type": "UserWidget", + "default_value": "/Game/MagicCard/UI/Hall/Menu/W_StartLogo.W_StartLogo_C" + }, + { + "id": "P5", + "name": "Owning Player", + "type": "Object", + "sub_type": "PlayerController" + } + ], + "output_pins": [ + { + "id": "P2", + "name": "", + "connected": true + }, + { + "id": "P4", + "name": "Return Value", + "type": "Object", + "sub_type": "W_StartLogo", + "connected": true + } + ] + }, + { + "id": "N41", + "type": "CallFunction", + "name": "Add to Viewport", + "member_parent": "UserWidget", + "member_name": "AddToViewport", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "Target", + "type": "Object", + "sub_type": "UserWidget", + "connected": true + }, + { + "id": "P4", + "name": "ZOrder", + "type": "Integer", + "default_value": "5" + } + ], + "output_pins": [ + { + "id": "P2", + "name": "" + } + ] + }, + { + "id": "N40", + "type": "VariableSet", + "name": "MenuRef", + "member_parent": "object/W_StartLogo", + "member_name": "MenuRef", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "Menu Ref", + "type": "Object", + "sub_type": "W_StartLogo", + "connected": true + } + ], + "output_pins": [ + { + "id": "P2", + "name": "", + "connected": true + }, + { + "id": "P4", + "name": "", + "type": "Object", + "sub_type": "W_StartLogo", + "connected": true + } + ] + }, + { + "id": "N38", + "type": "CallFunction", + "name": "Hide Main Menu", + "member_parent": "BP_MCGameInstance", + "member_name": "HideMainMenu", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "Target", + "type": "Object", + "sub_type": "self" + } + ], + "output_pins": [ + { + "id": "P2", + "name": "", + "connected": true + } + ] + }, + { + "id": "N42", + "type": "CallFunction", + "name": "Play Menu Anim", + "member_parent": "W_StartLogo", + "member_name": "PlayMenuAnim", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "Target", + "type": "Object", + "sub_type": "W_StartLogo", + "connected": true + }, + { + "id": "P4", + "name": "Play Anim", + "type": "Boolean", + "default_value": "false", + "connected": true + } + ], + "output_pins": [ + { + "id": "P2", + "name": "", + "connected": true + } + ] + }, + { + "id": "N11", + "type": "CallFunction", + "name": "Show Main Menu", + "member_parent": "BP_MCGameInstance", + "member_name": "ShowMainMenu", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "Target", + "type": "Object", + "sub_type": "self" + }, + { + "id": "P4", + "name": "With Anim", + "type": "Boolean", + "default_value": "true" + } + ], + "output_pins": [ + { + "id": "P2", + "name": "", + "connected": true + } + ] + }, + { + "id": "N16", + "type": "VariableSet", + "name": "IsJoiningSession", + "member_parent": "bool", + "member_name": "IsJoiningSession", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "Is Joining Session", + "type": "Boolean", + "default_value": "true" + } + ], + "output_pins": [ + { + "id": "P2", + "name": "", + "connected": true + }, + { + "id": "P4", + "name": "", + "type": "Boolean", + "default_value": "false" + } + ] + }, + { + "id": "N22", + "type": "Branch", + "name": "Branch", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P2", + "name": "Condition", + "type": "Boolean", + "default_value": "true", + "connected": true + } + ], + "output_pins": [ + { + "id": "P3", + "name": "True", + "connected": true + }, + { + "id": "P4", + "name": "False", + "connected": true + } + ] + }, + { + "id": "N44", + "type": "VariableGet", + "name": "IsJoiningSession", + "member_parent": "bool", + "member_name": "IsJoiningSession", + "pure": true, + "input_pins": [], + "output_pins": [ + { + "id": "P1", + "name": "Is Joining Session", + "type": "Boolean", + "default_value": "false", + "connected": true + } + ] + }, + { + "id": "N20", + "type": "VariableSet", + "name": "IsJoiningSession", + "member_parent": "bool", + "member_name": "IsJoiningSession", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "Is Joining Session", + "type": "Boolean", + "default_value": "false" + } + ], + "output_pins": [ + { + "id": "P2", + "name": "" + }, + { + "id": "P4", + "name": "", + "type": "Boolean", + "default_value": "false" + } + ] + }, + { + "id": "N45", + "type": "VariableGet", + "name": "IsJoiningSession", + "member_parent": "bool", + "member_name": "IsJoiningSession", + "pure": true, + "input_pins": [], + "output_pins": [ + { + "id": "P1", + "name": "Is Joining Session", + "type": "Boolean", + "default_value": "false", + "connected": true + } + ] + }, + { + "id": "N2", + "type": "Branch", + "name": "Branch", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P2", + "name": "Condition", + "type": "Boolean", + "default_value": "true", + "connected": true + } + ], + "output_pins": [ + { + "id": "P3", + "name": "True" + }, + { + "id": "P4", + "name": "False", + "connected": true + } + ] + }, + { + "id": "N6", + "type": "VariableSet", + "name": "IsJoiningSession", + "member_parent": "bool", + "member_name": "IsJoiningSession", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "Is Joining Session", + "type": "Boolean", + "default_value": "true" + } + ], + "output_pins": [ + { + "id": "P2", + "name": "" + }, + { + "id": "P4", + "name": "", + "type": "Boolean", + "default_value": "false" + } + ] + }, + { + "id": "N4", + "type": "VariableSet", + "name": "IsJoiningSession", + "member_parent": "bool", + "member_name": "IsJoiningSession", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "Is Joining Session", + "type": "Boolean", + "default_value": "false" + } + ], + "output_pins": [ + { + "id": "P2", + "name": "" + }, + { + "id": "P4", + "name": "", + "type": "Boolean", + "default_value": "false" + } + ] + }, + { + "id": "N26", + "type": "VariableSet", + "name": "IsLocalGame", + "member_parent": "bool", + "member_name": "IsLocalGame", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "Is Local Game", + "type": "Boolean", + "default_value": "false" + } + ], + "output_pins": [ + { + "id": "P2", + "name": "", + "connected": true + }, + { + "id": "P4", + "name": "", + "type": "Boolean", + "default_value": "false" + } + ] + }, + { + "id": "N28", + "type": "VariableSet", + "name": "IsLocalGame", + "member_parent": "bool", + "member_name": "IsLocalGame", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "Is Local Game", + "type": "Boolean", + "default_value": "false" + } + ], + "output_pins": [ + { + "id": "P2", + "name": "", + "connected": true + }, + { + "id": "P4", + "name": "", + "type": "Boolean", + "default_value": "false" + } + ] + }, + { + "id": "N43", + "type": "CallFunction", + "name": "Print String", + "member_parent": "KismetSystemLibrary", + "member_name": "PrintString", + "input_pins": [ + { + "id": "P1", + "name": "", + "connected": true + }, + { + "id": "P3", + "name": "In String", + "type": "String", + "default_value": "Already Joining Session" + }, + { + "id": "P4", + "name": "Print to Screen", + "type": "Boolean", + "default_value": "true" + }, + { + "id": "P5", + "name": "Print to Log", + "type": "Boolean", + "default_value": "true" + }, + { + "id": "P6", + "name": "Text Color", + "type": "Struct", + "sub_type": "LinearColor", + "default_value": "(R=0.000000,G=0.660000,B=1.000000,A=1.000000)" + }, + { + "id": "P7", + "name": "Duration", + "type": "Real", + "sub_type": "float", + "default_value": "2.000000" + }, + { + "id": "P8", + "name": "Key", + "type": "Name", + "default_value": "None", + "is_const": true + } + ], + "output_pins": [ + { + "id": "P2", + "name": "" + } + ] + }, + { + "id": "N46", + "type": "VariableGet", + "name": "Level Name", + "member_parent": "name", + "member_name": "Level Name", + "pure": true, + "input_pins": [], + "output_pins": [ + { + "id": "P1", + "name": "Level Name", + "type": "Name", + "default_value": "None", + "connected": true + } + ] + } + ], + "flows": + { + "execution": [ + "N1->N2", + "N3->N4", + "N5->N6", + "N7->N4", + "N9->N10", + "N10->N11", + "N13->N14", + "N15->N16", + "N15->N17", + "N15->N18", + "N17->N20", + "N18->N20", + "N21->N22", + "N14->N24", + "N25->N5", + "N25->N26", + "N25->N7", + "N27->N28", + "N30->N25", + "N31->N32", + "N32->N33", + "N33->N35", + "N36->N37", + "N37->N38", + "N39->N40", + "N40->N42", + "N38->N39", + "N42->N41", + "N11->N13", + "N16->N19", + "N22->N43", + "N22->N27", + "N2->N30", + "N26->N3", + "N28->N15" + ], + "data": + { + "N12.P2": "N24.P3", + "N21.P3": "N15.P6", + "N23.P2": "N27.P5", + "N9.P3": "N14.P4", + "N8.P2": "N25.P6", + "N29.P2": "N30.P5", + "N34.P1": "N35.P3", + "N39.P4": "N40.P3", + "N40.P4": "N42.P3", + "N36.P3": "N42.P4", + "N44.P1": "N22.P2", + "N45.P1": "N2.P2", + "N46.P1": "N3.P3" + } + } + } + ], + "structs": [], + "enums": [] + } \ No newline at end of file diff --git a/README.md b/README.md index 3e39c88..ddfc065 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ ### 💬 **Blueprint Communication Solved** - 🆕 Support for Claude 4, Gemini 2.5 Flash, & OpenAI's latest models - 🆕 Support for LM Studio -> [Quick Start Guide](https://github.com/protospatial/NodeToCode/wiki/LM-Studio-Quick-Start) +- 🆕 **Complete Blueprint Translation:** Translate entire Blueprints with all graphs, variables, and components at once +- 🆕 **Variable & Component Support:** Full support for Blueprint variables, local variables, and component overrides - **Pseudocode Translation:** Convert Blueprints into universally understandable pseudocode - **Automatic Translation Saving:** Each translation is saved locally for easy archiving & sharing via chat or docs - Share complete Blueprint logic as text in forums, chat, emails, or documentation @@ -45,6 +47,8 @@ ## 🔧 Under the Hood - **Blueprint Analysis:** Captures your entire Blueprint graph structure, including execution flows, data connections, variable references, and comments +- **Complete Blueprint Translation:** Translate entire Blueprints with all graphs, variables, and components in a single operation +- **Variable & Component Support:** Full support for Blueprint-level variables, local variables, and component property overrides - **Multiple LLM Options:** Use cloud providers (OpenAI, Anthropic Claude, Google Gemini, DeepSeek) or run 100% locally via Ollama for complete privacy - **Efficient Serialization:** Converts blueprints into a bespoke JSON schema that reduces token usage by 60-90% compared to UE's verbose Blueprint text format - **Style Guidance:** Supply your own C++ files as reference to maintain your project's coding standards and patterns @@ -57,6 +61,29 @@ - **Project Leads:** Improve team communication and maintain better system documentation - **Educators & Students:** Bridge the visual-to-code learning gap with real examples +## 🆕 Recent Updates + +### Complete Blueprint Translation +- **Translate Entire Blueprint:** New toolbar command to translate all graphs in a Blueprint at once +- **Batch Processing:** Each graph generates independent JSON and LLM requests, all sharing the same root directory +- **Error Feedback:** Comprehensive success/failure reporting for batch translations + +### Enhanced Variable & Component Support +- **Blueprint Variables:** Full support for Blueprint-level variables with complete type mapping (including Array, Set, Map) +- **Local Variables:** Function-level local variable collection and serialization +- **Component Overrides:** Automatic detection and serialization of component property overrides +- **Component Hierarchy:** Support for component parent-child relationships +- **Map Key Types:** Improved Map key type extraction from Blueprint pin types + +### ClassItSelf Graph Type +- **Class Structure Marking:** New graph type for representing class skeleton structure +- **C++ Class Generation:** Enables LLM to generate proper C++ class declarations, constructors, and member variables +- **Conditional Creation:** Automatically created when variables or components are present + +### Code Generation Improvements +- **Enhanced Prompts:** Significantly expanded CodeGen_CPP.md with detailed guidance for components, variables, class structures, event handling, and network replication +- **Better Type Mapping:** Improved handling of complex Unreal Engine types + ## 🏃Get Started ### :arrow_down: [Install the Plugin](https://github.com/protospatial/NodeToCode/releases) diff --git a/Source/Private/Core/N2CEditorIntegration.cpp b/Source/Private/Core/N2CEditorIntegration.cpp index affff4b..415a585 100644 --- a/Source/Private/Core/N2CEditorIntegration.cpp +++ b/Source/Private/Core/N2CEditorIntegration.cpp @@ -30,6 +30,283 @@ FN2CEditorIntegration& FN2CEditorIntegration::Get() return Instance; } +void FN2CEditorIntegration::ExecuteTranslateEntireBlueprintForEditor(TWeakPtr InEditor) +{ + // Check if translation is already in progress + UN2CLLMModule* LLMModule = UN2CLLMModule::Get(); + if (LLMModule && LLMModule->GetSystemStatus() == EN2CSystemStatus::Processing) + { + FN2CLogger::Get().LogWarning(TEXT("Translation already in progress, please wait")); + return; + } + + // Show the window as a tab + FGlobalTabmanager::Get()->TryInvokeTab(SN2CEditorWindow::TabId); + + // Get the editor pointer + TSharedPtr Editor = InEditor.Pin(); + if (!Editor.IsValid()) + { + FN2CLogger::Get().LogError(TEXT("Invalid Blueprint Editor pointer")); + return; + } + + // Get focused graph to resolve owning blueprint + UEdGraph* FocusedGraph = Editor->GetFocusedGraph(); + if (!FocusedGraph) + { + FN2CLogger::Get().LogError(TEXT("No focused graph in Blueprint Editor")); + return; + } + + UBlueprint* OwnerBP = Cast(FocusedGraph->GetOuter()); + if (!OwnerBP) + { + FN2CLogger::Get().LogError(TEXT("Focused graph has no owning Blueprint")); + return; + } + + const UN2CSettings* Settings = GetDefault(); + const bool bIncludeVariables = Settings ? Settings->bIncludeVariables : true; + + const FString BlueprintName = OwnerBP->GetName(); + + FN2CLogger::Get().Log( + FString::Printf(TEXT("Starting full Blueprint translation for: %s"), *BlueprintName), + EN2CLogSeverity::Info + ); + + if (!LLMModule || !LLMModule->Initialize()) + { + FN2CLogger::Get().LogError(TEXT("Failed to initialize LLM Module")); + return; + } + + // Begin batch translation - all graphs in this Blueprint will share the same root directory + LLMModule->BeginBatchTranslation(BlueprintName); + + // Use a single Blueprint-wide translation to build the full FN2CBlueprint, + // which already contains all graphs (including the synthetic ClassItSelf), + // then emit one JSON per graph by slicing the Graphs array. + FN2CNodeTranslator& Translator = FN2CNodeTranslator::Get(); + + if (!Translator.GenerateFromBlueprint(OwnerBP, bIncludeVariables)) + { + FN2CLogger::Get().LogError(TEXT("Failed to generate Blueprint-wide translation for Translate Entire Blueprint")); + // End batch translation on error + if (LLMModule) + { + LLMModule->EndBatchTranslation(); + } + return; + } + + const FN2CBlueprint& FullBlueprint = Translator.GetN2CBlueprint(); + if (!FullBlueprint.IsValid()) + { + FN2CLogger::Get().LogError(TEXT("Blueprint-wide node translation validation failed for Translate Entire Blueprint")); + // End batch translation on error + if (LLMModule) + { + LLMModule->EndBatchTranslation(); + } + return; + } + + FN2CLogger::Get().Log(TEXT("Blueprint-wide translation successful for Translate Entire Blueprint"), EN2CLogSeverity::Info); + + // First pass: build per-graph JSON payloads so we know how many requests + // will be sent for this Blueprint (used for batch completion logging). + TArray> PendingRequests; // Json, GraphName + TArray SerializationFailedGraphs; // Track graphs that failed JSON serialization + + for (const FN2CGraph& Graph : FullBlueprint.Graphs) + { + const FString GraphName = Graph.Name; + if (GraphName.IsEmpty()) + { + continue; + } + + FN2CBlueprint PerGraphBlueprint; + PerGraphBlueprint.Version = FullBlueprint.Version; + PerGraphBlueprint.Metadata = FullBlueprint.Metadata; + PerGraphBlueprint.Variables = FullBlueprint.Variables; + PerGraphBlueprint.Components = FullBlueprint.Components; + PerGraphBlueprint.Structs = FullBlueprint.Structs; + PerGraphBlueprint.Enums = FullBlueprint.Enums; + PerGraphBlueprint.Graphs.Reset(1); + PerGraphBlueprint.Graphs.Add(Graph); + + FN2CSerializer::SetPrettyPrint(false); + FString JsonOutput = FN2CSerializer::ToJson(PerGraphBlueprint); + if (JsonOutput.IsEmpty()) + { + FN2CLogger::Get().LogError(FString::Printf(TEXT("JSON serialization failed for graph: %s"), *GraphName)); + SerializationFailedGraphs.Add(GraphName); + continue; + } + + PendingRequests.Emplace(JsonOutput, GraphName); + } + + if (PendingRequests.Num() == 0) + { + FN2CLogger::Get().LogWarning(TEXT("No valid graphs to translate for this Blueprint")); + // End batch translation since there are no requests + if (LLMModule) + { + LLMModule->EndBatchTranslation(); + } + return; + } + + // Shared counter and result tracking for batch completion logging. + // We keep these local to the editor integration and update them from the per-request callback. + const int32 TotalRequests = PendingRequests.Num(); + const int32 TotalGraphs = FullBlueprint.Graphs.Num(); // Total including serialization failures + TSharedRef RemainingResponses = MakeShared(TotalRequests); + TSharedRef> SuccessfulGraphs = MakeShared>(); + TSharedRef> FailedGraphs = MakeShared>(SerializationFailedGraphs); // Include serialization failures + + FN2CLogger::Get().Log( + FString::Printf(TEXT("Starting batch translation: %d graphs to translate for Blueprint: %s (%d requests queued, %d failed serialization)"), + TotalGraphs, *BlueprintName, TotalRequests, SerializationFailedGraphs.Num()), + EN2CLogSeverity::Info + ); + + // Second pass: actually send requests to the LLM + for (const TPair& Request : PendingRequests) + { + const FString& JsonOutput = Request.Key; + const FString& GraphName = Request.Value; + + FN2CLogger::Get().Log( + FString::Printf(TEXT("Sending translation request for graph: %s"), *GraphName), + EN2CLogSeverity::Debug + ); + FN2CLogger::Get().Log(TEXT("JSON Output:"), EN2CLogSeverity::Debug); + FN2CLogger::Get().Log(JsonOutput, EN2CLogSeverity::Debug); + + LLMModule->ProcessN2CJson(JsonOutput, FOnLLMResponseReceived::CreateLambda( + [GraphName, RemainingResponses, BlueprintName, SuccessfulGraphs, FailedGraphs, TotalGraphs](const FString& Response) + { + bool bSuccess = false; + FString ErrorMessage; + + FN2CLogger::Get().Log( + FString::Printf(TEXT("Received LLM response for graph: %s"), *GraphName), + EN2CLogSeverity::Debug + ); + FN2CLogger::Get().Log(FString::Printf(TEXT("LLM Response for graph %s:\n\n%s"), *GraphName, *Response), EN2CLogSeverity::Debug); + + FN2CTranslationResponse TranslationResponse; + TScriptInterface ActiveService = UN2CLLMModule::Get()->GetActiveService(); + if (ActiveService.GetInterface()) + { + UN2CResponseParserBase* Parser = ActiveService->GetResponseParser(); + if (Parser) + { + if (Parser->ParseLLMResponse(Response, TranslationResponse)) + { + FN2CLogger::Get().Log( + FString::Printf(TEXT("Successfully parsed LLM response for graph: %s"), *GraphName), + EN2CLogSeverity::Info + ); + bSuccess = true; + SuccessfulGraphs->Add(GraphName); + } + else + { + ErrorMessage = TEXT("Failed to parse LLM response"); + FN2CLogger::Get().LogError( + FString::Printf(TEXT("Failed to parse LLM response for graph: %s"), *GraphName) + ); + FailedGraphs->Add(GraphName); + } + } + else + { + ErrorMessage = TEXT("No response parser available"); + FN2CLogger::Get().LogError( + FString::Printf(TEXT("No response parser available for graph: %s"), *GraphName) + ); + FailedGraphs->Add(GraphName); + } + } + else + { + ErrorMessage = TEXT("No active LLM service"); + FN2CLogger::Get().LogError( + FString::Printf(TEXT("No active LLM service for graph: %s"), *GraphName) + ); + FailedGraphs->Add(GraphName); + } + + // Decrement remaining counter and log summary when the batch completes. + const int32 NewRemaining = --(*RemainingResponses); + if (NewRemaining <= 0) + { + // Build summary message + FString Summary = FString::Printf( + TEXT("Full Blueprint translation complete for: %s\n") + TEXT(" Total graphs: %d\n") + TEXT(" Successful: %d\n") + TEXT(" Failed: %d"), + *BlueprintName, + TotalGraphs, + SuccessfulGraphs->Num(), + FailedGraphs->Num() + ); + + if (FailedGraphs->Num() > 0) + { + Summary += TEXT("\n Failed graphs: "); + for (int32 i = 0; i < FailedGraphs->Num(); ++i) + { + if (i > 0) + { + Summary += TEXT(", "); + } + Summary += (*FailedGraphs)[i]; + } + } + + if (SuccessfulGraphs->Num() > 0) + { + Summary += TEXT("\n Successful graphs: "); + for (int32 i = 0; i < SuccessfulGraphs->Num(); ++i) + { + if (i > 0) + { + Summary += TEXT(", "); + } + Summary += (*SuccessfulGraphs)[i]; + } + } + + // Log summary with appropriate severity + if (FailedGraphs->Num() > 0) + { + FN2CLogger::Get().LogWarning(Summary); + } + else + { + FN2CLogger::Get().Log(Summary, EN2CLogSeverity::Info); + } + + // End batch translation - clear the batch root path + UN2CLLMModule* BatchLLMModule = UN2CLLMModule::Get(); + if (BatchLLMModule) + { + BatchLLMModule->EndBatchTranslation(); + } + } + } + )); + } +} + void FN2CEditorIntegration::ExecuteCopyJsonForEditor(TWeakPtr InEditor) { FN2CLogger::Get().Log(TEXT("ExecuteCopyJsonForEditor called"), EN2CLogSeverity::Debug); @@ -300,6 +577,28 @@ void FN2CEditorIntegration::RegisterToolbarForEditor(TSharedPtrGetCurrentMode() == FBlueprintEditorApplicationModes::StandardBlueprintEditorMode; }) ); + + // Map the Translate Entire Blueprint command + CommandList->MapAction( + FN2CToolbarCommand::Get().TranslateEntireBlueprintCommand, + FExecuteAction::CreateLambda([this, WeakEditor, BlueprintName]() + { + FN2CLogger::Get().Log( + FString::Printf(TEXT("Translate Entire Blueprint triggered for Blueprint: %s"), *BlueprintName), + EN2CLogSeverity::Info + ); + ExecuteTranslateEntireBlueprintForEditor(WeakEditor); + }), + FCanExecuteAction::CreateLambda([WeakEditor]() + { + TSharedPtr Editor = WeakEditor.Pin(); + if (!Editor.IsValid()) + { + return false; + } + return Editor->GetCurrentMode() == FBlueprintEditorApplicationModes::StandardBlueprintEditorMode; + }) + ); // Map the Copy JSON command CommandList->MapAction( @@ -350,6 +649,7 @@ void FN2CEditorIntegration::RegisterToolbarForEditor(TSharedPtr& CollectedNode return N2CBlueprint.Graphs.Num() > 0; } +bool FN2CNodeTranslator::GenerateFromBlueprint(UBlueprint* InBlueprint, bool bIncludeVariables) +{ + N2CBlueprint = FN2CBlueprint(); + NodeIDMap.Empty(); + PinIDMap.Empty(); + ProcessedStructPaths.Empty(); + ProcessedEnumPaths.Empty(); + AdditionalGraphsToProcess.Empty(); + + if (!InBlueprint) + { + FN2CLogger::Get().LogWarning(TEXT("No Blueprint provided to GenerateFromBlueprint")); + return false; + } + + // Metadata + N2CBlueprint.Metadata.Name = InBlueprint->GetName(); + if (InBlueprint->GeneratedClass) + { + N2CBlueprint.Metadata.BlueprintClass = GetCleanClassName(InBlueprint->GeneratedClass->GetName()); + } + else if (InBlueprint->SkeletonGeneratedClass) + { + N2CBlueprint.Metadata.BlueprintClass = GetCleanClassName(InBlueprint->SkeletonGeneratedClass->GetName()); + } + switch (InBlueprint->BlueprintType) + { + case BPTYPE_Const: N2CBlueprint.Metadata.BlueprintType = EN2CBlueprintType::Const; break; + case BPTYPE_MacroLibrary: N2CBlueprint.Metadata.BlueprintType = EN2CBlueprintType::MacroLibrary; break; + case BPTYPE_Interface: N2CBlueprint.Metadata.BlueprintType = EN2CBlueprintType::Interface; break; + case BPTYPE_LevelScript: N2CBlueprint.Metadata.BlueprintType = EN2CBlueprintType::LevelScript; break; + case BPTYPE_FunctionLibrary: N2CBlueprint.Metadata.BlueprintType = EN2CBlueprintType::FunctionLibrary; break; + default: N2CBlueprint.Metadata.BlueprintType = EN2CBlueprintType::Normal; break; + } + + // Process graphs: event, functions, macros + TArray Graphs; + if (InBlueprint->UbergraphPages.Num() > 0) + { + Graphs.Append(InBlueprint->UbergraphPages); + } + if (InBlueprint->FunctionGraphs.Num() > 0) + { + Graphs.Append(InBlueprint->FunctionGraphs); + } + if (InBlueprint->MacroGraphs.Num() > 0) + { + Graphs.Append(InBlueprint->MacroGraphs); + } + + for (UEdGraph* Graph : Graphs) + { + if (!Graph) + { + continue; + } + CurrentDepth = 0; + ProcessGraph(Graph, DetermineGraphType(Graph)); + } + + // Collect component overrides defined on this Blueprint (SimpleConstructionScript) + CollectComponentOverrides(InBlueprint); + + // Variables + if (bIncludeVariables) + { + for (const FBPVariableDescription& Desc : InBlueprint->NewVariables) + { + FN2CVariable Var; + ConvertVariableDescription(Desc, Var); + N2CBlueprint.Variables.Add(Var); + } + } + + // Synthesize a dedicated ClassItSelf graph to carry only class-level structure + // (UCLASS, class declaration, UPROPERTY members, ctor/dtor) separate from EventGraph logic. + // + // NOTE: ClassItSelf is a synthetic graph type that does not correspond to any actual Blueprint graph. + // It serves as a marker for the LLM to generate class-level C++ code (class declaration, constructor, + // destructor, member variables, and component declarations). The actual class-level data (variables + // and components) are stored at the Blueprint level (N2CBlueprint.Variables and N2CBlueprint.Components), + // not within this graph. The graph itself is intentionally empty (no nodes) because it represents + // class structure, not executable flow. + { + FN2CGraph ClassItSelfGraph; + ClassItSelfGraph.Name = TEXT("ClassItSelf"); + ClassItSelfGraph.GraphType = EN2CGraphType::ClassItSelf; + // Intentionally leave this graph empty - it's a marker for class-level code generation + // The LLM will use this graph type along with Blueprint-level Variables[] and Components[] + // arrays to generate the class skeleton in C++ + N2CBlueprint.Graphs.Add(ClassItSelfGraph); + } + FString Ctx = FString::Printf( + TEXT("Generated from Blueprint: %s (Graphs=%d, Vars=%d, Components=%d)"), + *N2CBlueprint.Metadata.Name, + N2CBlueprint.Graphs.Num(), + N2CBlueprint.Variables.Num(), + N2CBlueprint.Components.Num()); + FN2CLogger::Get().Log(TEXT("Blueprint translation complete"), EN2CLogSeverity::Info, Ctx); + return N2CBlueprint.Graphs.Num() > 0; +} + +void FN2CNodeTranslator::CollectComponentOverrides(UBlueprint* InBlueprint) +{ + if (!InBlueprint) + { + return; + } + + USimpleConstructionScript* SCS = InBlueprint->SimpleConstructionScript; + if (!SCS) + { + return; + } + + const TArray& AllNodes = SCS->GetAllNodes(); + if (AllNodes.Num() == 0) + { + return; + } + + for (USCS_Node* Node : AllNodes) + { + if (!Node) + { + continue; + } + + UActorComponent* Template = Node->ComponentTemplate; + if (!Template) + { + continue; + } + + UClass* ComponentClass = Template->GetClass(); + if (!ComponentClass) + { + continue; + } + + UActorComponent* ClassDefaultObject = Cast(ComponentClass->GetDefaultObject()); + if (!ClassDefaultObject) + { + continue; + } + FN2CComponentOverride ComponentOverride; + ComponentOverride.ComponentName = Node->GetVariableName().ToString(); + ComponentOverride.ComponentClassName = GetCleanClassName(ComponentClass->GetName()); + + // Try to get parent component information + // Note: Different UE versions have different APIs for accessing SCS node hierarchy + ComponentOverride.AttachParentName = TEXT(""); + + // For SceneComponents, try to find parent through AttachParent property + if (USceneComponent* SceneComp = Cast(Template)) + { + // Check AttachParent property (FObjectProperty pointing to the parent component) + // FindField is a template function, need to use the correct syntax + FObjectProperty* AttachParentProp = CastField( + ComponentClass->FindPropertyByName(TEXT("AttachParent"))); + + if (AttachParentProp) + { + UObject* AttachParentObj = AttachParentProp->GetObjectPropertyValue( + AttachParentProp->ContainerPtrToValuePtr(Template)); + + if (USceneComponent* AttachParent = Cast(AttachParentObj)) + { + // Find the SCS node that corresponds to this parent component + // by matching the component template + for (USCS_Node* OtherNode : AllNodes) + { + if (OtherNode && OtherNode->ComponentTemplate == AttachParent) + { + ComponentOverride.AttachParentName = OtherNode->GetVariableName().ToString(); + FN2CLogger::Get().Log(FString::Printf( + TEXT("Component '%s' has parent '%s' from AttachParent property"), + *ComponentOverride.ComponentName, + *ComponentOverride.AttachParentName), + EN2CLogSeverity::Debug); + break; + } + } + } + } + } + + // If still empty, try to infer from SCS tree structure by checking if this is the root + // Root component is typically the first node or the one without AttachParent + if (ComponentOverride.AttachParentName.IsEmpty() && AllNodes.Num() > 0) + { + // The root component is usually the first node in the SCS tree + // or we can check if it's the default root component + if (AllNodes[0] == Node) + { + // Likely the root component, no parent + ComponentOverride.AttachParentName = TEXT(""); + FN2CLogger::Get().Log(FString::Printf( + TEXT("Component '%s' is likely the root component (no parent)"), + *ComponentOverride.ComponentName), + EN2CLogSeverity::Debug); + } + } + // Diff properties between the Blueprint component template and the class CDO + for (TFieldIterator PropIt(ComponentClass, EFieldIteratorFlags::IncludeSuper); PropIt; ++PropIt) + { + FProperty* Property = *PropIt; + if (!Property) + { + continue; + } + + // Skip transient or non-config properties that are unlikely to be Blueprint-edited defaults + if (Property->HasAnyPropertyFlags(CPF_Transient | CPF_DuplicateTransient | CPF_TextExportTransient)) + { + continue; + } + + // If values are identical, skip + if (Property->Identical_InContainer(Template, ClassDefaultObject)) + { + continue; + } + + FN2CVariable Var; + Var.Name = Property->GetName(); + + // Map property type to EN2CStructMemberType and TypeName when possible + Var.Type = ConvertPropertyToStructMemberType(Property); + + if (FStructProperty* StructProp = CastField(Property)) + { + if (UScriptStruct* Struct = StructProp->Struct) + { + Var.TypeName = Struct->GetName(); + } + } + else if (FObjectProperty* ObjProp = CastField(Property)) + { + if (UClass* ObjClass = ObjProp->PropertyClass) + { + Var.TypeName = ObjClass->GetName(); + } + } + else if (FClassProperty* ClassProp = CastField(Property)) + { + if (UClass* MetaClass = ClassProp->MetaClass) + { + Var.TypeName = MetaClass->GetName(); + } + } + else if (FEnumProperty* EnumProp = CastField(Property)) + { + if (UEnum* Enum = EnumProp->GetEnum()) + { + Var.TypeName = Enum->GetName(); + } + } + + // Export the overridden value as text + FString ExportedValue; + void* TemplateValuePtr = Property->ContainerPtrToValuePtr(Template); + void* DefaultValuePtr = Property->ContainerPtrToValuePtr(ClassDefaultObject); + Property->ExportText_Direct(ExportedValue, TemplateValuePtr, DefaultValuePtr, nullptr, PPF_None); + Var.DefaultValue = ExportedValue; + + ComponentOverride.OverriddenProperties.Add(Var); + } + + const bool bHasOverrides = ComponentOverride.OverriddenProperties.Num() > 0; + + // Treat components whose template was not created as Native as + // Blueprint-added (non-native) components. These should always be + // declared and constructed in the ClassItSelf graph, even if they + // have no property overrides. For native/inherited components we only + // include them when there are overridden properties to initialize. + const bool bIsBlueprintAddedComponent = (Template->CreationMethod != EComponentCreationMethod::Native); + + if (bIsBlueprintAddedComponent || bHasOverrides) + { + N2CBlueprint.Components.Add(ComponentOverride); + } + } +} + +void FN2CNodeTranslator::ConvertVariableDescription(const FBPVariableDescription& Desc, FN2CVariable& OutVar) +{ + OutVar = FN2CVariable(); + OutVar.Name = Desc.VarName.ToString(); + + // Map type from FEdGraphPinType + const FEdGraphPinType& T = Desc.VarType; + const FString& Cat = T.PinCategory.ToString(); + + // Map basic types + if (Cat == TEXT("bool")) + { + OutVar.Type = EN2CStructMemberType::Bool; + } + else if (Cat == TEXT("byte")) + { + OutVar.Type = EN2CStructMemberType::Byte; + } + else if (Cat == TEXT("int")) + { + OutVar.Type = EN2CStructMemberType::Int; + } + else if (Cat == TEXT("float")) + { + OutVar.Type = EN2CStructMemberType::Float; + } + else if (Cat == TEXT("string")) + { + OutVar.Type = EN2CStructMemberType::String; + } + else if (Cat == TEXT("name")) + { + OutVar.Type = EN2CStructMemberType::Name; + } + else if (Cat == TEXT("text")) + { + OutVar.Type = EN2CStructMemberType::Text; + } + else if (Cat == TEXT("struct")) + { + OutVar.Type = EN2CStructMemberType::Struct; + OutVar.TypeName = T.PinSubCategoryObject.IsValid() ? T.PinSubCategoryObject->GetName() : FString(); + } + else if (Cat == TEXT("class")) + { + OutVar.Type = EN2CStructMemberType::Class; + OutVar.TypeName = T.PinSubCategoryObject.IsValid() ? T.PinSubCategoryObject->GetName() : FString(); + } + else if (Cat == TEXT("object")) + { + OutVar.Type = EN2CStructMemberType::Object; + OutVar.TypeName = T.PinSubCategoryObject.IsValid() ? T.PinSubCategoryObject->GetName() : FString(); + } + else if (Cat == TEXT("enum")) + { + OutVar.Type = EN2CStructMemberType::Enum; + OutVar.TypeName = T.PinSubCategoryObject.IsValid() ? T.PinSubCategoryObject->GetName() : FString(); + } + else + { + OutVar.Type = EN2CStructMemberType::Custom; + } + + // Handle container types + OutVar.bIsArray = T.ContainerType == EPinContainerType::Array; + OutVar.bIsSet = T.ContainerType == EPinContainerType::Set; + OutVar.bIsMap = T.ContainerType == EPinContainerType::Map; + + // Extract Map key type from PinValueType + if (OutVar.bIsMap) + { + // Try to extract key type from PinValueType (for Map containers) + if (T.PinValueType.TerminalCategory != NAME_None) + { + const FString KeyCategory = T.PinValueType.TerminalCategory.ToString(); + + // Map key category to EN2CStructMemberType + if (KeyCategory == TEXT("bool")) + { + OutVar.KeyType = EN2CStructMemberType::Bool; + } + else if (KeyCategory == TEXT("byte")) + { + OutVar.KeyType = EN2CStructMemberType::Byte; + } + else if (KeyCategory == TEXT("int") || KeyCategory == TEXT("int64")) + { + OutVar.KeyType = EN2CStructMemberType::Int; + } + else if (KeyCategory == TEXT("float") || KeyCategory == TEXT("double")) + { + OutVar.KeyType = EN2CStructMemberType::Float; + } + else if (KeyCategory == TEXT("string")) + { + OutVar.KeyType = EN2CStructMemberType::String; + } + else if (KeyCategory == TEXT("name")) + { + OutVar.KeyType = EN2CStructMemberType::Name; + } + else if (KeyCategory == TEXT("text")) + { + OutVar.KeyType = EN2CStructMemberType::Text; + } + else if (KeyCategory == TEXT("struct")) + { + OutVar.KeyType = EN2CStructMemberType::Struct; + if (T.PinValueType.TerminalSubCategoryObject.IsValid()) + { + OutVar.KeyTypeName = T.PinValueType.TerminalSubCategoryObject->GetName(); + } + } + else if (KeyCategory == TEXT("enum")) + { + OutVar.KeyType = EN2CStructMemberType::Enum; + if (T.PinValueType.TerminalSubCategoryObject.IsValid()) + { + OutVar.KeyTypeName = T.PinValueType.TerminalSubCategoryObject->GetName(); + } + } + else + { + OutVar.KeyType = EN2CStructMemberType::Custom; + } + } + else + { + // Fallback: if PinValueType is not available, use Custom + OutVar.KeyType = EN2CStructMemberType::Custom; + FN2CLogger::Get().LogWarning(FString::Printf( + TEXT("Map variable '%s' has no key type information in PinValueType, using Custom"), + *OutVar.Name)); + } + } + + // Set default value if available + OutVar.DefaultValue = Desc.DefaultValue; +} + +void FN2CNodeTranslator::CollectLocalVariables(UK2Node_FunctionEntry* EntryNode, TArray& OutVariables) +{ + if (!EntryNode) + { + return; + } + + OutVariables.Empty(); + + for (const FBPVariableDescription& Desc : EntryNode->LocalVariables) + { + FN2CVariable Var; + ConvertVariableDescription(Desc, Var); + OutVariables.Add(Var); + } +} + FString FN2CNodeTranslator::GenerateNodeID() { return FString::Printf(TEXT("N%d"), NodeIDMap.Num() + 1); @@ -314,6 +762,17 @@ bool FN2CNodeTranslator::ProcessGraph(UEdGraph* Graph, EN2CGraphType GraphType) FN2CGraph NewGraph; NewGraph.Name = Graph->GetName(); NewGraph.GraphType = GraphType; + + // Collect function-local variables from the entry node (if any) + for (UEdGraphNode* Node : Graph->Nodes) + { + if (UK2Node_FunctionEntry* EntryNode = Cast(Node)) + { + CollectLocalVariables(EntryNode, NewGraph.LocalVariables); + break; + } + } + CurrentGraph = &NewGraph; // Collect and process nodes from this graph diff --git a/Source/Private/Core/N2CSerializer.cpp b/Source/Private/Core/N2CSerializer.cpp index 02f56b7..3ce73a0 100644 --- a/Source/Private/Core/N2CSerializer.cpp +++ b/Source/Private/Core/N2CSerializer.cpp @@ -127,6 +127,48 @@ TSharedPtr FN2CSerializer::BlueprintToJsonObject(const FN2CBlueprin } JsonObject->SetArrayField(TEXT("enums"), EnumsArray); + // Add variables array + TArray> VarsArray; + for (const FN2CVariable& Var : Blueprint.Variables) + { + TSharedPtr VarObject = VariableToJsonObject(Var); + if (VarObject.IsValid()) + { + VarsArray.Add(MakeShared(VarObject)); + } + } + JsonObject->SetArrayField(TEXT("variables"), VarsArray); + + // Add components array + TArray> ComponentsArray; + for (const FN2CComponentOverride& Component : Blueprint.Components) + { + TSharedPtr ComponentObject = MakeShared(); + + ComponentObject->SetStringField(TEXT("component_name"), Component.ComponentName); + ComponentObject->SetStringField(TEXT("component_class_name"), Component.ComponentClassName); + + if (!Component.AttachParentName.IsEmpty()) + { + ComponentObject->SetStringField(TEXT("attach_parent_name"), Component.AttachParentName); + } + + // Serialize overridden properties as an array of variable objects + TArray> OverriddenPropsArray; + for (const FN2CVariable& Var : Component.OverriddenProperties) + { + TSharedPtr VarObject = VariableToJsonObject(Var); + if (VarObject.IsValid()) + { + OverriddenPropsArray.Add(MakeShared(VarObject)); + } + } + ComponentObject->SetArrayField(TEXT("overridden_properties"), OverriddenPropsArray); + + ComponentsArray.Add(MakeShared(ComponentObject)); + } + JsonObject->SetArrayField(TEXT("components"), ComponentsArray); + return JsonObject; } @@ -399,6 +441,50 @@ TSharedPtr FN2CSerializer::EnumToJsonObject(const FN2CEnum& Enum) return JsonObject; } +TSharedPtr FN2CSerializer::VariableToJsonObject(const FN2CVariable& Var) +{ + TSharedPtr JsonObject = MakeShared(); + + JsonObject->SetStringField(TEXT("name"), Var.Name); + JsonObject->SetStringField(TEXT("type"), + StaticEnum()->GetNameStringByValue(static_cast(Var.Type))); + + if (!Var.TypeName.IsEmpty()) + { + JsonObject->SetStringField(TEXT("type_name"), Var.TypeName); + } + + if (Var.bIsArray) + { + JsonObject->SetBoolField(TEXT("is_array"), true); + } + if (Var.bIsSet) + { + JsonObject->SetBoolField(TEXT("is_set"), true); + } + if (Var.bIsMap) + { + JsonObject->SetBoolField(TEXT("is_map"), true); + JsonObject->SetStringField(TEXT("key_type"), + StaticEnum()->GetNameStringByValue(static_cast(Var.KeyType))); + if (!Var.KeyTypeName.IsEmpty()) + { + JsonObject->SetStringField(TEXT("key_type_name"), Var.KeyTypeName); + } + } + + if (!Var.DefaultValue.IsEmpty()) + { + JsonObject->SetStringField(TEXT("default_value"), Var.DefaultValue); + } + if (!Var.Comment.IsEmpty()) + { + JsonObject->SetStringField(TEXT("comment"), Var.Comment); + } + + return JsonObject; +} + bool FN2CSerializer::ParseBlueprintFromJson(const TSharedPtr& JsonObject, FN2CBlueprint& OutBlueprint) { if (!JsonObject.IsValid()) @@ -494,6 +580,78 @@ bool FN2CSerializer::ParseBlueprintFromJson(const TSharedPtr& JsonO return ValidGraphCount > 0; // Return true if we got at least one valid graph } + // Optionally parse variables array + const TArray>* VarsArray; + if (JsonObject->TryGetArrayField(TEXT("variables"), VarsArray)) + { + OutBlueprint.Variables.Empty(); + for (const TSharedPtr& VarValue : *VarsArray) + { + const TSharedPtr& VarObject = VarValue->AsObject(); + if (!VarObject.IsValid()) + { + continue; + } + + FN2CVariable Var; + if (ParseVariableFromJson(VarObject, Var)) + { + OutBlueprint.Variables.Add(Var); + } + } + } + + return true; +} + +bool FN2CSerializer::ParseVariableFromJson(const TSharedPtr& JsonObject, FN2CVariable& OutVar) +{ + if (!JsonObject.IsValid()) + { + return false; + } + + FString Name, TypeString; + if (!JsonObject->TryGetStringField(TEXT("name"), Name) || + !JsonObject->TryGetStringField(TEXT("type"), TypeString)) + { + FN2CLogger::Get().LogError(TEXT("Missing required variable fields in JSON")); + return false; + } + + OutVar.Name = Name; + + int64 TypeValue = StaticEnum()->GetValueByNameString(TypeString, EGetByNameFlags::None); + if (TypeValue == INDEX_NONE) + { + FN2CLogger::Get().LogError(FString::Printf(TEXT("Invalid variable type: %s"), *TypeString)); + return false; + } + OutVar.Type = static_cast(TypeValue); + + // Optional fields + JsonObject->TryGetStringField(TEXT("type_name"), OutVar.TypeName); + JsonObject->TryGetBoolField(TEXT("is_array"), OutVar.bIsArray); + JsonObject->TryGetBoolField(TEXT("is_set"), OutVar.bIsSet); + JsonObject->TryGetBoolField(TEXT("is_map"), OutVar.bIsMap); + + if (OutVar.bIsMap) + { + FString KeyTypeString; + if (JsonObject->TryGetStringField(TEXT("key_type"), KeyTypeString)) + { + int64 KeyTypeValue = StaticEnum()->GetValueByNameString(KeyTypeString, EGetByNameFlags::None); + if (KeyTypeValue != INDEX_NONE) + { + OutVar.KeyType = static_cast(KeyTypeValue); + } + } + JsonObject->TryGetStringField(TEXT("key_type_name"), OutVar.KeyTypeName); + } + + JsonObject->TryGetStringField(TEXT("default_value"), OutVar.DefaultValue); + JsonObject->TryGetStringField(TEXT("comment"), OutVar.Comment); + return true; } diff --git a/Source/Private/Core/N2CToolbarCommand.cpp b/Source/Private/Core/N2CToolbarCommand.cpp index b986894..63893b0 100644 --- a/Source/Private/Core/N2CToolbarCommand.cpp +++ b/Source/Private/Core/N2CToolbarCommand.cpp @@ -11,12 +11,15 @@ const FName FN2CToolbarCommand::CommandName_Open = TEXT("NodeToCode_OpenWindow"); const FName FN2CToolbarCommand::CommandName_Collect = TEXT("NodeToCode_CollectNodes"); const FName FN2CToolbarCommand::CommandName_CopyJson = TEXT("NodeToCode_CopyJson"); +const FName FN2CToolbarCommand::CommandName_TranslateEntire = TEXT("NodeToCode_TranslateEntireBlueprint"); const FText FN2CToolbarCommand::CommandLabel_Open = NSLOCTEXT("NodeToCode", "OpenWindow", "Open Node to Code"); const FText FN2CToolbarCommand::CommandLabel_Collect = NSLOCTEXT("NodeToCode", "CollectNodes", "Collect and Translate Nodes"); const FText FN2CToolbarCommand::CommandLabel_CopyJson = NSLOCTEXT("NodeToCode", "CopyJson", "Copy Blueprint JSON"); +const FText FN2CToolbarCommand::CommandLabel_TranslateEntire = NSLOCTEXT("NodeToCode", "TranslateEntireBlueprint", "Translate Entire Blueprint"); const FText FN2CToolbarCommand::CommandTooltip_Open = NSLOCTEXT("NodeToCode", "OpenWindowTooltip", "Open the Node to Code window"); const FText FN2CToolbarCommand::CommandTooltip_Collect = NSLOCTEXT("NodeToCode", "CollectNodesTooltip", "Collect nodes from current Blueprint graph and translate to code"); const FText FN2CToolbarCommand::CommandTooltip_CopyJson = NSLOCTEXT("NodeToCode", "CopyJsonTooltip", "Copy the serialized Blueprint JSON to clipboard"); +const FText FN2CToolbarCommand::CommandTooltip_TranslateEntire = NSLOCTEXT("NodeToCode", "TranslateEntireTooltip", "Translate all graphs in the owning Blueprint (functions, macros, event graphs).\nRespects 'Include Variables' setting."); FN2CToolbarCommand::FN2CToolbarCommand() : TCommands( @@ -56,6 +59,14 @@ void FN2CToolbarCommand::RegisterCommands() FInputChord() ); + UI_COMMAND( + TranslateEntireBlueprintCommand, + "Translate Entire Blueprint", + "Translate all graphs in the owning Blueprint to code. Results will be shown in the Node to Code window.", + EUserInterfaceActionType::Button, + FInputChord() + ); + FN2CLogger::Get().Log(TEXT("N2C toolbar commands registered"), EN2CLogSeverity::Debug); } diff --git a/Source/Private/LLM/N2CLLMModule.cpp b/Source/Private/LLM/N2CLLMModule.cpp index bfe85a9..bdf2dd0 100644 --- a/Source/Private/LLM/N2CLLMModule.cpp +++ b/Source/Private/LLM/N2CLLMModule.cpp @@ -208,6 +208,24 @@ void UN2CLLMModule::OpenTranslationFolder(bool& Success) } +void UN2CLLMModule::BeginBatchTranslation(const FString& BlueprintName) +{ + // Generate a shared root path for this batch + FString BlueprintNameToUse = BlueprintName; + if (BlueprintNameToUse.IsEmpty()) + { + BlueprintNameToUse = TEXT("UnknownBlueprint"); + } + CurrentBatchRootPath = GenerateTranslationRootPath(BlueprintNameToUse); + FN2CLogger::Get().Log(FString::Printf(TEXT("Batch translation started, root path: %s"), *CurrentBatchRootPath), EN2CLogSeverity::Info); +} + +void UN2CLLMModule::EndBatchTranslation() +{ + CurrentBatchRootPath.Empty(); + FN2CLogger::Get().Log(TEXT("Batch translation ended"), EN2CLogSeverity::Info); +} + bool UN2CLLMModule::SaveTranslationToDisk(const FN2CTranslationResponse& Response, const FN2CBlueprint& Blueprint) { // Get blueprint name from metadata @@ -217,8 +235,18 @@ bool UN2CLLMModule::SaveTranslationToDisk(const FN2CTranslationResponse& Respons BlueprintName = TEXT("UnknownBlueprint"); } - // Generate root path for this translation - FString RootPath = GenerateTranslationRootPath(BlueprintName); + // Use batch root path if in batch mode, otherwise generate a new timestamped path for each translation + FString RootPath; + if (!CurrentBatchRootPath.IsEmpty()) + { + // Batch mode: reuse the shared root path + RootPath = CurrentBatchRootPath; + } + else + { + // Single translation mode: generate a new timestamped directory for each translation + RootPath = GenerateTranslationRootPath(BlueprintName); + } // Ensure the directory exists if (!EnsureDirectoryExists(RootPath)) @@ -310,8 +338,209 @@ bool UN2CLLMModule::SaveTranslationToDisk(const FN2CTranslationResponse& Respons const UN2CSettings* Settings = GetDefault(); EN2CCodeLanguage TargetLanguage = Settings ? Settings->TargetLanguage : EN2CCodeLanguage::Cpp; + + + // Determine if we're in batch mode (when CurrentBatchRootPath is set) + const bool bIsBatchMode = !CurrentBatchRootPath.IsEmpty(); + + // Use batch-specific features for batch translations, original logic for single translations + if (bIsBatchMode) + { + SaveGraphFilesWithBatchFeatures(Response, RootPath, TargetLanguage); + } + else + { + SaveGraphFilesOriginal(Response, RootPath, TargetLanguage); + } + + FN2CLogger::Get().Log(FString::Printf(TEXT("Translation saved to: %s"), *RootPath), EN2CLogSeverity::Info); + return true; +} + +void UN2CLLMModule::SaveGraphFilesWithBatchFeatures( + const FN2CTranslationResponse& Response, + const FString& RootPath, + EN2CCodeLanguage TargetLanguage) const +{ + const bool bIsCpp = (TargetLanguage == EN2CCodeLanguage::Cpp); + + // Helper to sanitize graph names for use as filesystem paths while keeping the + // original GraphName intact for logical/JSON purposes. + auto SanitizeNameForFilesystem = [](const FString& InName) -> FString + { + FString Result = InName; + Result = Result.TrimStartAndEnd(); + + // Replace Windows-invalid filename characters with underscores + const TCHAR InvalidChars[] = + { + TEXT('<'), TEXT('>'), TEXT(':'), TEXT('"'), + TEXT('/'), TEXT('\\'), TEXT('|'), TEXT('?'), TEXT('*') + }; + + for (TCHAR Ch : InvalidChars) + { + FString From; + From.AppendChar(Ch); + Result.ReplaceInline(*From, TEXT("_"), ESearchCase::CaseSensitive); + } + + return Result; + }; + // Save each graph's files for (const FN2CGraphTranslation& Graph : Response.Graphs) + { + // Skip graphs with empty names + if (Graph.GraphName.IsEmpty()) + { + continue; + } + + const FString SanitizedGraphName = SanitizeNameForFilesystem(Graph.GraphName); + const bool bIsClassItSelf = Graph.GraphType.Equals(TEXT("ClassItSelf"), ESearchCase::IgnoreCase); + const bool bHasGraphClass = !Graph.GraphClass.IsEmpty(); + + // Log graph information for debugging + FN2CLogger::Get().Log( + FString::Printf(TEXT("[SaveGraphFiles] Processing graph: Name='%s', Type='%s', Class='%s', IsClassItSelf=%d, HasGraphClass=%d"), + *Graph.GraphName, *Graph.GraphType, *Graph.GraphClass, bIsClassItSelf ? 1 : 0, bHasGraphClass ? 1 : 0), + EN2CLogSeverity::Debug); + + // For ClassItSelf graphs with a class name, save directly to class-centric directory + // Skip the graph-specific directory to avoid duplicate files + if (bIsClassItSelf && bHasGraphClass) + { + FString ClassDir = FPaths::Combine(RootPath, Graph.GraphClass); + if (!EnsureDirectoryExists(ClassDir)) + { + FN2CLogger::Get().LogWarning(FString::Printf(TEXT("Failed to create class directory: %s"), *ClassDir)); + continue; + } + + // Save declaration file (C++ only) + if (bIsCpp && !Graph.Code.GraphDeclaration.IsEmpty()) + { + FString ClassHeaderPath = FPaths::Combine(ClassDir, Graph.GraphClass + TEXT(".h")); + FN2CLogger::Get().Log( + FString::Printf(TEXT("[SaveGraphFiles] Saving ClassItSelf header to class-centric path: %s (Graph: %s)"), + *ClassHeaderPath, *Graph.GraphName), + EN2CLogSeverity::Debug); + if (!FFileHelper::SaveStringToFile(Graph.Code.GraphDeclaration, *ClassHeaderPath)) + { + FN2CLogger::Get().LogWarning(FString::Printf(TEXT("Failed to save class header file: %s"), *ClassHeaderPath)); + } + else + { + FN2CLogger::Get().Log( + FString::Printf(TEXT("[SaveGraphFiles] Successfully saved class header file: %s"), *ClassHeaderPath), + EN2CLogSeverity::Debug); + } + } + + // Save implementation file + if (!Graph.Code.GraphImplementation.IsEmpty()) + { + FString Extension = GetFileExtensionForLanguage(TargetLanguage); + FString ClassImplPath = FPaths::Combine(ClassDir, Graph.GraphClass + Extension); + FN2CLogger::Get().Log( + FString::Printf(TEXT("[SaveGraphFiles] Saving ClassItSelf implementation to class-centric path: %s (Graph: %s)"), + *ClassImplPath, *Graph.GraphName), + EN2CLogSeverity::Debug); + if (!FFileHelper::SaveStringToFile(Graph.Code.GraphImplementation, *ClassImplPath)) + { + FN2CLogger::Get().LogWarning(FString::Printf(TEXT("Failed to save class implementation file: %s"), *ClassImplPath)); + } + else + { + FN2CLogger::Get().Log( + FString::Printf(TEXT("[SaveGraphFiles] Successfully saved class implementation file: %s"), *ClassImplPath), + EN2CLogSeverity::Debug); + } + } + + // Save implementation notes to class directory + if (!Graph.Code.ImplementationNotes.IsEmpty()) + { + FString NotesPath = FPaths::Combine(ClassDir, Graph.GraphClass + TEXT("_Notes.txt")); + if (!FFileHelper::SaveStringToFile(Graph.Code.ImplementationNotes, *NotesPath)) + { + FN2CLogger::Get().LogWarning(FString::Printf(TEXT("Failed to save notes file: %s"), *NotesPath)); + } + } + + // Skip normal graph directory processing for ClassItSelf graphs + continue; + } + + // For non-ClassItSelf graphs (or ClassItSelf without class name), use normal graph directory + FString GraphDir = FPaths::Combine(RootPath, SanitizedGraphName); + if (!EnsureDirectoryExists(GraphDir)) + { + FN2CLogger::Get().LogWarning(FString::Printf(TEXT("Failed to create graph directory: %s"), *GraphDir)); + continue; + } + + const FString FileBaseName = SanitizedGraphName; + + // Save declaration file (C++ only) + if (bIsCpp && !Graph.Code.GraphDeclaration.IsEmpty()) + { + FString HeaderPath = FPaths::Combine(GraphDir, FileBaseName + TEXT(".h")); + FN2CLogger::Get().Log( + FString::Printf(TEXT("[SaveGraphFiles] Saving header file: %s (Graph: %s)"), *HeaderPath, *Graph.GraphName), + EN2CLogSeverity::Debug); + if (!FFileHelper::SaveStringToFile(Graph.Code.GraphDeclaration, *HeaderPath)) + { + FN2CLogger::Get().LogWarning(FString::Printf(TEXT("Failed to save header file: %s"), *HeaderPath)); + } + else + { + FN2CLogger::Get().Log( + FString::Printf(TEXT("[SaveGraphFiles] Successfully saved header file: %s"), *HeaderPath), + EN2CLogSeverity::Debug); + } + } + + // Save implementation file with appropriate extension + if (!Graph.Code.GraphImplementation.IsEmpty()) + { + FString Extension = GetFileExtensionForLanguage(TargetLanguage); + FString ImplPath = FPaths::Combine(GraphDir, FileBaseName + Extension); + FN2CLogger::Get().Log( + FString::Printf(TEXT("[SaveGraphFiles] Saving implementation file: %s (Graph: %s)"), *ImplPath, *Graph.GraphName), + EN2CLogSeverity::Debug); + if (!FFileHelper::SaveStringToFile(Graph.Code.GraphImplementation, *ImplPath)) + { + FN2CLogger::Get().LogWarning(FString::Printf(TEXT("Failed to save implementation file: %s"), *ImplPath)); + } + else + { + FN2CLogger::Get().Log( + FString::Printf(TEXT("[SaveGraphFiles] Successfully saved implementation file: %s"), *ImplPath), + EN2CLogSeverity::Debug); + } + } + + // Save implementation notes + if (!Graph.Code.ImplementationNotes.IsEmpty()) + { + FString NotesPath = FPaths::Combine(GraphDir, FileBaseName + TEXT("_Notes.txt")); + if (!FFileHelper::SaveStringToFile(Graph.Code.ImplementationNotes, *NotesPath)) + { + FN2CLogger::Get().LogWarning(FString::Printf(TEXT("Failed to save notes file: %s"), *NotesPath)); + } + } + } +} + +void UN2CLLMModule::SaveGraphFilesOriginal( + const FN2CTranslationResponse& Response, + const FString& RootPath, + EN2CCodeLanguage TargetLanguage) const +{ + // Save each graph's files using original simple logic + for (const FN2CGraphTranslation& Graph : Response.Graphs) { // Skip graphs with empty names if (Graph.GraphName.IsEmpty()) @@ -360,7 +589,6 @@ bool UN2CLLMModule::SaveTranslationToDisk(const FN2CTranslationResponse& Respons } FN2CLogger::Get().Log(FString::Printf(TEXT("Translation saved to: %s"), *RootPath), EN2CLogSeverity::Info); - return true; } FString UN2CLLMModule::GenerateTranslationRootPath(const FString& BlueprintName) const diff --git a/Source/Private/Utils/Validators/N2CBlueprintValidator.cpp b/Source/Private/Utils/Validators/N2CBlueprintValidator.cpp index 4dcbde1..63ce017 100644 --- a/Source/Private/Utils/Validators/N2CBlueprintValidator.cpp +++ b/Source/Private/Utils/Validators/N2CBlueprintValidator.cpp @@ -69,20 +69,27 @@ bool FN2CBlueprintValidator::ValidateRequired(const FN2CBlueprint& Blueprint, FS bool FN2CBlueprintValidator::ValidateGraphs(const FN2CBlueprint& Blueprint, FString& OutError) { - // Check that at least one graph has nodes - bool bHasNodes = false; + // Check that at least one non-ClassItSelf graph has nodes. Blueprints + // that consist only of synthetic ClassItSelf graphs are allowed, since + // they carry class-level structure but no executable flow. + bool bHasNonSkeletonGraph = false; + bool bHasNodesInNonSkeletonGraph = false; for (const FN2CGraph& Graph : Blueprint.Graphs) { - if (Graph.Nodes.Num() > 0) + if (Graph.GraphType != EN2CGraphType::ClassItSelf) { - bHasNodes = true; - break; + bHasNonSkeletonGraph = true; + if (Graph.Nodes.Num() > 0) + { + bHasNodesInNonSkeletonGraph = true; + break; + } } } - if (!bHasNodes) + if (bHasNonSkeletonGraph && !bHasNodesInNonSkeletonGraph) { - OutError = TEXT("No nodes found in any graph"); + OutError = TEXT("No nodes found in any non-ClassItSelf graph"); FN2CLogger::Get().LogError(OutError); return false; } @@ -110,7 +117,24 @@ bool FN2CBlueprintValidator::ValidateGraph(const FN2CGraph& Graph, FString& OutE return false; } - // Check nodes array + // Special-case synthetic ClassItSelf graphs: they are allowed to have + // no nodes, because they only carry class-level structure (UCLASS, + // UPROPERTY members, ctor/dtor, components) and do not represent an + // executable flow graph. For these graphs we skip node/flow validation + // and treat them as structurally valid. + if (Graph.GraphType == EN2CGraphType::ClassItSelf) + { + if (Graph.Nodes.Num() == 0) + { + FN2CLogger::Get().Log( + FString::Printf(TEXT("ClassItSelf graph %s has no nodes (expected for synthetic class skeleton graphs)"), *Graph.Name), + EN2CLogSeverity::Debug + ); + return true; + } + } + + // Check nodes array for all non-ClassItSelf graphs if (Graph.Nodes.Num() == 0) { OutError = FString::Printf(TEXT("No nodes in graph %s"), *Graph.Name); diff --git a/Source/Public/Core/N2CEditorIntegration.h b/Source/Public/Core/N2CEditorIntegration.h index 8125da6..fc722fe 100644 --- a/Source/Public/Core/N2CEditorIntegration.h +++ b/Source/Public/Core/N2CEditorIntegration.h @@ -57,6 +57,9 @@ class FN2CEditorIntegration /** Execute copy blueprint JSON to clipboard for a specific editor */ void ExecuteCopyJsonForEditor(TWeakPtr InEditor); + /** Execute translate entire blueprint (all graphs) for a specific editor */ + void ExecuteTranslateEntireBlueprintForEditor(TWeakPtr InEditor); + /** Handle asset editor opened callback */ void HandleAssetEditorOpened(UObject* Asset, IAssetEditorInstance* EditorInstance); diff --git a/Source/Public/Core/N2CNodeTranslator.h b/Source/Public/Core/N2CNodeTranslator.h index b2b3e82..f3929b8 100644 --- a/Source/Public/Core/N2CNodeTranslator.h +++ b/Source/Public/Core/N2CNodeTranslator.h @@ -29,6 +29,9 @@ class FN2CNodeTranslator */ bool GenerateN2CStruct(const TArray& CollectedNodes); + /** Generate N2CBlueprint from entire Blueprint (all graphs, optional variables) */ + bool GenerateFromBlueprint(class UBlueprint* InBlueprint, bool bIncludeVariables = true); + /** * @brief Get the generated Blueprint structure * @return The translated Blueprint structure @@ -150,4 +153,13 @@ class FN2CNodeTranslator /** Log detailed debug information about the node */ void LogNodeDetails(const FN2CNodeDefinition& NodeDef); + + /** Collect Blueprint component instances and their overridden default properties */ + void CollectComponentOverrides(class UBlueprint* InBlueprint); + + /** Convert FBPVariableDescription to FN2CVariable */ + void ConvertVariableDescription(const struct FBPVariableDescription& Desc, struct FN2CVariable& OutVar); + + /** Collect local variables from a function entry node */ + void CollectLocalVariables(class UK2Node_FunctionEntry* EntryNode, TArray& OutVariables); }; diff --git a/Source/Public/Core/N2CSerializer.h b/Source/Public/Core/N2CSerializer.h index 697fb52..b30c2d7 100644 --- a/Source/Public/Core/N2CSerializer.h +++ b/Source/Public/Core/N2CSerializer.h @@ -37,6 +37,7 @@ class FN2CSerializer static TSharedPtr FlowsToJsonObject(const FN2CFlows& Flows); static TSharedPtr StructToJsonObject(const FN2CStruct& Struct); static TSharedPtr EnumToJsonObject(const FN2CEnum& Enum); + static TSharedPtr VariableToJsonObject(const FN2CVariable& Var); /** JSON parsing helpers */ static bool ParseBlueprintFromJson(const TSharedPtr& JsonObject, FN2CBlueprint& OutBlueprint); @@ -46,6 +47,7 @@ class FN2CSerializer static bool ParseFlowsFromJson(const TSharedPtr& JsonObject, FN2CFlows& OutFlows); static bool ParseStructFromJson(const TSharedPtr& JsonObject, FN2CStruct& OutStruct); static bool ParseEnumFromJson(const TSharedPtr& JsonObject, FN2CEnum& OutEnum); + static bool ParseVariableFromJson(const TSharedPtr& JsonObject, FN2CVariable& OutVar); /** Formatting configuration */ static bool bPrettyPrint; diff --git a/Source/Public/Core/N2CSettings.h b/Source/Public/Core/N2CSettings.h index baa3676..effabb2 100644 --- a/Source/Public/Core/N2CSettings.h +++ b/Source/Public/Core/N2CSettings.h @@ -471,6 +471,11 @@ class NODETOCODE_API UN2CSettings : public UDeveloperSettings UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category = "Node to Code | Code Generation", meta=(DisplayName="Max Translation Depth", ClampMin="0", ClampMax="5", UIMin="0", UIMax="5")) int32 TranslationDepth = 0; + + /** Include Blueprint variables in serialization output */ + UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category = "Node to Code | Code Generation", + meta=(DisplayName="Include Variables")) + bool bIncludeVariables = true; /** Minimum severity level for logging */ UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category = "Node to Code | Logging") diff --git a/Source/Public/Core/N2CToolbarCommand.h b/Source/Public/Core/N2CToolbarCommand.h index 63de7dd..83325d2 100644 --- a/Source/Public/Core/N2CToolbarCommand.h +++ b/Source/Public/Core/N2CToolbarCommand.h @@ -17,15 +17,19 @@ class FN2CToolbarCommand : public TCommands TSharedPtr OpenWindowCommand; TSharedPtr CollectNodesCommand; TSharedPtr CopyJsonCommand; + TSharedPtr TranslateEntireBlueprintCommand; // Command names and labels static const FName CommandName_Open; static const FName CommandName_Collect; static const FName CommandName_CopyJson; + static const FName CommandName_TranslateEntire; static const FText CommandLabel_Open; static const FText CommandLabel_Collect; static const FText CommandLabel_CopyJson; + static const FText CommandLabel_TranslateEntire; static const FText CommandTooltip_Open; static const FText CommandTooltip_Collect; static const FText CommandTooltip_CopyJson; + static const FText CommandTooltip_TranslateEntire; }; diff --git a/Source/Public/LLM/N2CLLMModule.h b/Source/Public/LLM/N2CLLMModule.h index eeafa18..1815479 100644 --- a/Source/Public/LLM/N2CLLMModule.h +++ b/Source/Public/LLM/N2CLLMModule.h @@ -71,6 +71,12 @@ class NODETOCODE_API UN2CLLMModule : public UObject /** Save translation files to disk */ bool SaveTranslationToDisk(const FN2CTranslationResponse& Response, const FN2CBlueprint& Blueprint); + /** Begin a batch translation (e.g. Translate Entire Blueprint) - all translations in this batch will share the same root directory */ + void BeginBatchTranslation(const FString& BlueprintName); + + /** End a batch translation - clears the batch root path */ + void EndBatchTranslation(); + private: /** Generate file paths for translation */ FString GenerateTranslationRootPath(const FString& BlueprintName) const; @@ -83,6 +89,18 @@ class NODETOCODE_API UN2CLLMModule : public UObject /** Create directory if it doesn't exist */ bool EnsureDirectoryExists(const FString& DirectoryPath) const; + + /** Save graph files with batch-specific features (sanitized names, ClassItSelf special handling) */ + void SaveGraphFilesWithBatchFeatures( + const FN2CTranslationResponse& Response, + const FString& RootPath, + EN2CCodeLanguage TargetLanguage) const; + + /** Save graph files using original simple logic (for single translations) */ + void SaveGraphFilesOriginal( + const FN2CTranslationResponse& Response, + const FString& RootPath, + EN2CCodeLanguage TargetLanguage) const; /** Initialize components */ bool InitializeComponents(); @@ -120,6 +138,9 @@ class NODETOCODE_API UN2CLLMModule : public UObject UPROPERTY() FString LatestTranslationPath; + /** Cached root path for the current translation batch (e.g. one Translate Entire Blueprint run) */ + FString CurrentBatchRootPath; + /** Initialization state */ bool bIsInitialized; }; diff --git a/Source/Public/Models/N2CBlueprint.h b/Source/Public/Models/N2CBlueprint.h index f3f02eb..ce140ee 100644 --- a/Source/Public/Models/N2CBlueprint.h +++ b/Source/Public/Models/N2CBlueprint.h @@ -212,6 +212,98 @@ struct FN2CStruct bool IsValid() const; }; +/** + * @struct FN2CVariable + * @brief Represents a Blueprint variable declaration + */ +USTRUCT(BlueprintType) +struct FN2CVariable +{ + GENERATED_BODY() + + /** Variable name */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Node to Code") + FString Name; + + /** Variable type (basic category) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Node to Code") + EN2CStructMemberType Type; + + /** Full type name for objects/structs/enums/classes */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Node to Code") + FString TypeName; + + /** Container flags */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Node to Code") + bool bIsArray = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Node to Code") + bool bIsSet = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Node to Code") + bool bIsMap = false; + + /** Map key type if this variable is a Map */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Node to Code") + EN2CStructMemberType KeyType; + + /** Map key type name for complex keys */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Node to Code") + FString KeyTypeName; + + /** Default value as string (best-effort) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Node to Code") + FString DefaultValue; + + /** Optional comment/tooltip */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Node to Code") + FString Comment; + + FN2CVariable() + : Name(TEXT("")) + , Type(EN2CStructMemberType::Int) + , TypeName(TEXT("")) + , KeyType(EN2CStructMemberType::Int) + , KeyTypeName(TEXT("")) + , DefaultValue(TEXT("")) + , Comment(TEXT("")) + { + } +}; + +/** + * @struct FN2CComponentOverride + * @brief Represents a Blueprint component instance and its overridden default properties + */ +USTRUCT(BlueprintType) +struct FN2CComponentOverride +{ + GENERATED_BODY() + + /** Component instance name on the Blueprint (variable name on the actor) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Node to Code") + FString ComponentName; + + /** Component class name (e.g. StaticMeshComponent) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Node to Code") + FString ComponentClassName; + + /** Optional attach parent component/variable name */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Node to Code") + FString AttachParentName; + + /** Properties on this component whose defaults were overridden in the Blueprint */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Node to Code") + TArray OverriddenProperties; + + FN2CComponentOverride() + : ComponentName(TEXT("")) + , ComponentClassName(TEXT("")) + , AttachParentName(TEXT("")) + { + } +}; + /** * @struct FN2CEnumValue * @brief Represents a single value in an enum @@ -286,6 +378,8 @@ enum class EN2CGraphType : uint8 Construction UMETA(DisplayName = "Construction Script"), /** An animation graph */ Animation UMETA(DisplayName = "Animation"), + /** A synthetic class skeleton graph (no nodes, only class-level structure) */ + ClassItSelf UMETA(DisplayName = "Class Skeleton"), /** A struct definition */ Struct UMETA(DisplayName = "Struct"), /** An enum definition */ @@ -317,6 +411,10 @@ struct FN2CGraph UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Node to Code") FN2CFlows Flows; + /** Local variables declared for this graph (e.g. function-local variables) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Node to Code") + TArray LocalVariables; + FN2CGraph() : GraphType(EN2CGraphType::EventGraph) { @@ -355,6 +453,14 @@ struct FN2CBlueprint UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Node to Code") TArray Enums; + /** Array of declared Blueprint variables */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Node to Code") + TArray Variables; + + /** Array of Blueprint components and their overridden default properties */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Node to Code") + TArray Components; + FN2CBlueprint() { // Version is automatically initialized to "1.0.0" by FN2CVersion constructor