<!-- SUMMARY -->
# Universal Bridge — Quick Reference
`bridge = unreal.UNDUniversalBridge` — 5 mechanisms for ANY UE editor system:
1. **Graph Text Templates**: `export_graph_nodes_to_text(graph)` / `import_graph_nodes_from_text(graph, text)` — copy-paste nodes between any UEdGraph
2. **JSON Reflection**: `get_property_as_json(obj, "Path")` / `set_property_from_json(obj, "Path", "value")` — read/write ANY UPROPERTY on ANY UObject
3. **Dynamic Array & Object Access**:
   - `get_sub_object(obj, "EditorData.SubTrees[0]")` — navigate to nested/protected UObject properties
   - `add_instanced_struct_to_array(obj, "path", "StructType", '{json}')` — for TArray<FInstancedStruct> (ChooserTable elements, etc.)
   - `add_object_to_array(obj, "path", "ClassName", '{json}')` — for TArray<UObject*> (StateTree states, etc.)
4. **Schema Actions**: `query_graph_actions(graph, "keyword", 10)` → `perform_graph_action(graph, index, x, y)` — create nodes in ANY graph editor
5. **Spec Builders** (PREFERRED for batch):
   - `build_graph_from_spec(asset, json_spec)` — Material, BehaviorTree, Blueprint, any graph. Blueprint auto-delegates to K2-optimized path.
   - `build_data_from_spec(asset, json_spec)` — DataTable rows, UObject properties, StateTree elements
- **Rule of thumb**: Mechanism 5 for batch operations; 1-4 for individual ops/discovery
<!-- END SUMMARY -->

# UNDUniversalBridge — Universal UE Editor API Bridge

`unreal.UNDUniversalBridge` is a Python-accessible `UBlueprintFunctionLibrary` that provides
five universal mechanisms for manipulating **any** Unreal Engine editor system via reflection.

**When to use:** Use when you need to work with graph-based editors (ControlRig, BehaviorTree,
AnimBP, Niagara) or data-driven systems (StateTree, ChooserTable) where direct Python APIs
are incomplete or unavailable.

---

## Decision Tree — Choose the Right Approach

```
What do I need to do?
├── Create an asset (Material, BP, DataTable, etc.) → see asset_creation.md
├── Build a complete Blueprint graph from spec → BuildGraphFromSpec (auto-delegates to K2 path)
├── Build ANY other graph (Material, BT, AnimBP, Niagara, ControlRig, SoundCue)
│   ├── Batch (multiple nodes + connections) → BuildGraphFromSpec (universal path)
│   └── Individual nodes (interactive) → QueryGraphActions + PerformGraphAction
├── Access a UObject inside another UObject (protected or nested)
│   └── GetSubObject(obj, "EditorData") or GetSubObject(obj, "SubTrees[0]")
├── Read/write properties on ANY UObject
│   ├── Simple property → get/set_editor_property() (Python native)
│   ├── Complex/nested/protected property → GetPropertyAsJSON / SetPropertyFromJSON
│   └── Full object dump → SerializeObjectToJSON
├── Add elements to FInstancedStruct arrays (ChooserTable, etc.) → AddInstancedStructToArray
├── Add UObject elements to UObject* arrays (StateTree states, etc.) → AddObjectToArray
├── Bulk-create DataTable rows → BuildDataFromSpec with "rows"
├── Set properties on any UObject in bulk → BuildDataFromSpec with "properties"
└── Copy node clusters between graphs → ExportGraphNodesToText + ImportGraphNodesFromText
```

**Key principle:** If the system you need isn't documented anywhere — it still works through
these 5 mechanisms. Schema Actions discovers ANY graph node type. JSON Reflection reads/writes
ANY property. You never need system-specific code.

---

## Mechanism 1: Graph Text Templates

Export and import nodes as UE copy-paste text. Works with **any** `UEdGraph` subclass.

```python
import unreal
bridge = unreal.UNDUniversalBridge

# Export all nodes from a graph as copy-paste text
graph = ...  # any UEdGraph
text = bridge.export_graph_nodes_to_text(graph)

# Import nodes back into a compatible graph
imported_titles = bridge.import_graph_nodes_from_text(graph, text)
print(imported_titles)  # ["PrintString", "Sequence", ...]

# Check compatibility before importing
can_import = bridge.can_import_graph_nodes(graph, text)
```

**Use cases:** Duplicating node setups between graphs; backing up a complex node cluster;
migrating node layouts from one graph to another of the same type.

---

## Mechanism 2: JSON Reflection

Read and write **any** `UPROPERTY` on **any** `UObject` via JSON strings. Supports dot-notation
paths with array indices.

```python
import unreal
bridge = unreal.UNDUniversalBridge

# Read a property
value_json = bridge.get_property_as_json(obj, "PropertyName")
nested_json = bridge.get_property_as_json(obj, "EditorData.SubTrees[0].RootHash")

# Write a property — value is ALWAYS a JSON string
bridge.set_property_from_json(obj, "MaxValue", "100.0")
bridge.set_property_from_json(obj, "Origin", '{"X": 100.0, "Y": 200.0, "Z": 0.0}')
bridge.set_property_from_json(obj, "Tags[0]", '"MyTag"')

# Dot-path syntax for deeply nested properties
bridge.set_property_from_json(obj, "Config.Physics.Gravity.Z", "-980.0")
bridge.set_property_from_json(obj, "Slots[2].ItemClass", '"/Script/MyGame.MyItemClass"')
bridge.get_property_as_json(obj, "EditorData.SubTrees[0].States[1].Name")

# Serialize entire object to JSON (great for discovering structure)
full_json = bridge.serialize_object_to_json(obj)
print(full_json)

# Deserialize JSON into an existing object
bridge.deserialize_object_from_json(obj, '{"MaxValue": 200.0, "bIsActive": true}')
```

**Dot-path rules:**
- Use `.` to traverse struct members: `Config.SubStruct.Field`
- Use `[N]` for array indices: `Items[0].Name`
- Combine freely: `EditorData.SubTrees[0].States[1].Tasks[2].Duration`
- Works on ANY property — including protected, Transient, and nested structs that `set_editor_property` can't reach

**Use cases:** Reading StateTree/ChooserTable internal data; modifying properties that have no
direct Python API; batch-setting defaults on CDOs; inspecting any UObject structure.

---

## Mechanism 3: Dynamic Array Population

Two helpers for adding elements to typed arrays via reflection:

### 3a. AddInstancedStructToArray — for TArray<FInstancedStruct>

Used for **ChooserTable columns/results** and similar struct-based arrays.

```python
import unreal
bridge = unreal.UNDUniversalBridge

# Add a struct to an FInstancedStruct array
bridge.add_instanced_struct_to_array(
    owner_object,
    "Path.To.StructArray",            # array property path
    "GameplayTagMatchCondition",      # UScriptStruct type name
    '{"Tag": {"TagName": "Status.Active"}}'  # initial properties
)

# Remove element at index
bridge.remove_array_element(owner_object, "Path.To.StructArray", 2)
```

**Use query_ue_api first:** Discover exact struct type names before calling.

### 3b. GetSubObject — access protected/nested UObject properties

Some UPROPERTY fields are not accessible from Python's `get_editor_property()` (marked "protected"). Use `GetSubObject` to navigate to any UObject through property paths:

```python
import unreal
bridge = unreal.UNDUniversalBridge

asset = unreal.load_asset("/Game/AI/ST_EnemyAI")

# Access protected EditorData property:
editor_data = bridge.get_sub_object(asset, 'EditorData')

# Navigate into arrays — SubTrees[0] returns the root UStateTreeState:
root_state = bridge.get_sub_object(editor_data, 'SubTrees[0]')

# Chain navigation in one call:
root_state = bridge.get_sub_object(asset, 'EditorData.SubTrees[0]')

# Once you have the UObject, get_editor_property works on IT:
print(root_state.get_editor_property('Name'))
```

### 3c. AddObjectToArray — for TArray<UObject*>

Used for **StateTree states** and any system that stores UObject pointers in arrays.

**CRITICAL**: `UStateTreeState` is a **UObject**, NOT a UScriptStruct. Use `AddObjectToArray`, NOT `AddInstancedStructToArray`.

```python
import unreal
bridge = unreal.UNDUniversalBridge

# StateTree: get root state via GetSubObject (EditorData is protected!)
asset = unreal.load_asset("/Game/AI/ST_EnemyAI")
editor_data = bridge.get_sub_object(asset, 'EditorData')
root = bridge.get_sub_object(editor_data, 'SubTrees[0]')

# Add children to root (Children is TArray<UStateTreeState*> on UStateTreeState)
bridge.add_object_to_array(root, 'Children', 'StateTreeState', '{"Name":"Patrol"}')
bridge.add_object_to_array(root, 'Children', 'StateTreeState', '{"Name":"Combat"}')

# To add a NEW root state:
bridge.add_object_to_array(editor_data, 'SubTrees', 'StateTreeState', '{"Name":"NewRoot"}')
```

**Key difference:**
- `AddInstancedStructToArray` → for `TArray<FInstancedStruct>` (value types, polymorphic structs)
- `AddObjectToArray` → for `TArray<UObject*>` (reference types, UObject subclasses)

If you get error "is not TArray<FInstancedStruct>", the array holds UObjects — switch to `AddObjectToArray`.
If `get_editor_property` throws "protected", use `GetSubObject` to navigate to the object first.

---

## Mechanism 4: Schema Actions (Universal Graph Node Creation)

Query and execute right-click menu actions for **any** graph type. This is the **universal
fallback** — if you don't know what nodes exist in a graph, Schema Actions will find them.

> **Implementation note:** K2 graphs use `FBlueprintActionDatabase` (not `GetMenuEntries()`),
> so `QueryGraphActions` internally routes K2 graphs through `FBlueprintActionMenuUtils::MakePaletteMenu`
> for correct results. Non-K2 graphs use the standard `GetGraphContextActions` path.

```python
import unreal
bridge = unreal.UNDUniversalBridge
helpers = unreal.UNDBlueprintHelpers
lib = unreal.BlueprintEditorLibrary

# ── Blueprint K2 function graph ──────────────────────────────────────────
bp = unreal.load_asset("/Game/Components/BP_MyComponent")
func = lib.find_graph(bp, "MyFunction")

# Step 1: Query available actions (searched against Title + Category + Keywords)
actions = bridge.query_graph_actions(func, "subtract", 10)
for a in actions:
    print(a)
# → "[0] Math|Float > float - float"
# → "[1] Math|Double > Subtract (Double)"

# Step 2: Create the node via its action index
node = bridge.perform_graph_action(func, 0, pos_x=400, pos_y=100)
print(node.get_node_title())  # "float - float"

# ── Material graph: discover and create expression nodes ─────────────────
mat = unreal.load_asset("/Game/Materials/M_Rock")
mat_lib = unreal.MaterialEditingLibrary
graphs = mat_lib.get_material_graphs(mat)  # discover via dir()
if graphs:
    mat_graph = graphs[0]
    # Find available material expressions
    actions = bridge.query_graph_actions(mat_graph, "TextureSample", 10)
    for a in actions:
        print(a)  # "[0] Texture > TextureSample"
    # Create the node
    tex_node = bridge.perform_graph_action(mat_graph, 0, -400, 0)

# ── Non-K2 graph (BehaviorTree, ControlRig, AnimGraph) ───────────────────
bt = unreal.load_asset("/Game/AI/BT_MyTree")
# Get graph via dir() introspection or build_graph_from_spec
```

**CRITICAL:** `query_graph_actions` and `perform_graph_action` must use the **same graph
object** — the action index cache is per-graph instance.

**For Blueprint graphs — prefer `build_graph_from_spec`:** It handles K2 node creation
natively via the optimized Blueprint path. Use Mechanism 4 only when:
- Working with non-K2 graphs (ControlRig, BehaviorTree, AnimBP, Niagara, etc.)
- You need to discover available actions via `query_graph_actions`

**Typical workflow:**
1. `query_graph_actions(graph, "keyword", 20)` — find actions containing keyword
2. Inspect the returned list, identify the right `[index]`
3. `perform_graph_action(graph, index, posX, posY)` — create the node
4. For Blueprint graphs, prefer `build_graph_from_spec` instead (handles node creation + wiring atomically)

---

## Mechanism 5: Declarative Spec Builders

Build **entire graph structures** or **populate data assets** from a single JSON spec.
Covers ALL current and future UE editors — no new C++ code needed per editor type.

### BuildGraphFromSpec — ANY graph editor

Works with UBlueprint (K2), UMaterial, UBehaviorTree, and any UEdGraph-based editor.
Automatically routes to the optimal path:
- **Blueprint** → delegates to `UNDBlueprintHelpers.build_graph_from_spec` (K2-optimized)
- **Material** → uses `UMaterialEditingLibrary` (expression-aware)
- **Everything else** → universal path via Schema Actions + ConnectPins

```python
import unreal, json
bridge = unreal.UNDUniversalBridge

# ── Material: build a simple PBR setup ────────────────────────────────────
mat = unreal.load_asset("/Game/Materials/M_Rock")
spec = {
    "nodes": [
        {"id": "color",   "action_query": "VectorParameter",  "x": -400, "y": 0},
        {"id": "rough",   "action_query": "Constant",         "x": -400, "y": 200},
        {"id": "tex",     "action_query": "TextureSample",    "x": -400, "y": 400}
    ],
    "connections": [
        {"from": "color", "from_pin": "",    "to": "output", "to_pin": "BaseColor"},
        {"from": "rough", "from_pin": "",    "to": "output", "to_pin": "Roughness"},
        {"from": "tex",   "from_pin": "RGB", "to": "output", "to_pin": "Normal"}
    ],
    "node_properties": [
        {"node": "color", "path": "DefaultValue", "value": "{\"R\":0.5,\"G\":0.3,\"B\":0.1,\"A\":1}"},
        {"node": "rough", "path": "R",            "value": "0.7"}
    ]
}
result = json.loads(bridge.build_graph_from_spec(mat, json.dumps(spec)))
print(result["success"], result["nodes_created"], result["errors"])

# ── BehaviorTree: universal path (Schema Actions) ─────────────────────────
bt = unreal.load_asset("/Game/AI/BT_MyTree")
spec = {
    "nodes": [
        {"id": "sel",  "action_query": "Selector",  "x": 200, "y": 0},
        {"id": "wait", "action_query": "Wait",       "x": 400, "y": 0}
    ],
    "connections": [
        {"from": "root", "from_pin": "0",      "to": "sel",  "to_pin": "Parent"},
        {"from": "sel",  "from_pin": "0",      "to": "wait", "to_pin": "Parent"}
    ]
}
result = json.loads(bridge.build_graph_from_spec(bt, json.dumps(spec)))

# ── Blueprint: automatically delegates to K2-optimized path ───────────────
bp = unreal.load_asset("/Game/Blueprints/BP_MyActor")
spec = {
    "graph_type": "event_graph",
    "nodes": [
        {"id": "print", "type": "function_call", "function": "/Script/Engine.KismetSystemLibrary.PrintString"}
    ],
    "connections": [
        {"from": "begin_play", "from_pin": "then", "to": "print", "to_pin": "execute"}
    ],
    "pin_defaults": [
        {"node": "print", "pin": "InString", "value": "Hello World"}
    ]
}
result = json.loads(bridge.build_graph_from_spec(bp, json.dumps(spec)))
```

**JSON spec format (universal):**
- `nodes[]` — `id` (unique string), `action_query` (search text for Schema Actions), `x`/`y` (position)
- `connections[]` — `from`/`to` (node IDs), `from_pin`/`to_pin` (pin names)
- `pin_defaults[]` — `node` (ID), `pin` (name), `value` (string)
- `node_properties[]` — `node` (ID), `path` (property path), `value` (JSON value)
- `clear_existing` — optional bool, removes non-structural nodes before building

**Pre-seeded IDs** (available for connections without creating):
- `"output"` — Material output node, SoundCue output
- `"root"` — BehaviorTree root composite
- Blueprint events: `"begin_play"`, `"tick"`, etc. (K2 path)

### BuildDataFromSpec — ANY data-driven asset

Works with UDataTable, UDataAsset, StateTree, ChooserTable, and any UObject with UPROPERTY.

```python
import unreal, json
bridge = unreal.UNDUniversalBridge

# ── DataTable: add rows ───────────────────────────────────────────────────
dt = unreal.load_asset("/Game/Data/DT_MyTable")
spec = {
    "rows": [
        {"RowName": "Row1", "ValueA": 100, "ValueB": 150,  "Category": "TypeA"},
        {"RowName": "Row2", "ValueA": 50,  "ValueB": 1000, "Category": "TypeB"}
    ]
}
result = json.loads(bridge.build_data_from_spec(dt, json.dumps(spec)))
print(result["rows_added"])  # 2

# ── Any UObject: set properties via reflection ────────────────────────────
asset = unreal.load_asset("/Game/Data/DA_MyAsset")
spec = {
    "properties": {
        "DisplayName": "My Item",
        "Icon": "/Game/Icons/T_MyIcon",
        "Stats": {"ValueA": 10, "ValueB": 5.0}
    }
}
result = json.loads(bridge.build_data_from_spec(asset, json.dumps(spec)))

# ── StateTree: add FInstancedStruct elements ──────────────────────────────
st = unreal.load_asset("/Game/AI/ST_MyBehavior")
spec = {
    "struct_arrays": [{
        "path": "EditorData.SubTrees[0].Nodes",
        "elements": [
            {"struct": "StateTreeWaitTask",          "props": {"Duration": 2.0}},
            {"struct": "GameplayTagMatchCondition",  "props": {"Tag": {"TagName": "MyTag"}}}
        ]
    }]
}
result = json.loads(bridge.build_data_from_spec(st, json.dumps(spec)))
```

**JSON spec format:**
- `rows[]` — DataTable rows with `RowName` field (only for UDataTable)
- `properties` — object of property names → values (any UObject)
- `struct_arrays[]` — `path` to TArray<FInstancedStruct>, `elements[]` with `struct` type name + `props`

---

## Choosing the Right Mechanism

| Goal | Mechanism |
|------|-----------:|
| Build complete Material graph from spec | **5: BuildGraphFromSpec** |
| Build complete BehaviorTree/Niagara/any graph from spec | **5: BuildGraphFromSpec** |
| Populate DataTable with rows | **5: BuildDataFromSpec** |
| Set properties on any UObject | **5: BuildDataFromSpec** |
| Add StateTree tasks/conditions | **5: BuildDataFromSpec** |
| Build Blueprint K2 graphs from spec | **5: BuildGraphFromSpec** (auto-delegates to K2 path) |
| Copy node clusters between graphs | 1: Graph Text Templates |
| Read a single property | 2: JSON Reflection |
| Discover unknown object structure | 2: SerializeObjectToJSON |
| Write a protected/nested property | 2: SetPropertyFromJSON |
| Create individual nodes interactively | 4: Schema Actions |
| Discover what nodes exist in ANY graph | 4: QueryGraphActions |

**Rule of thumb:** Use Mechanism 5 (Spec Builders) for batch operations. Use Mechanisms 1-4
for individual operations, discovery, or when Mechanism 5 doesn't cover your use case.
Schema Actions (Mechanism 4) is the **universal fallback** — it works with ANY graph type
the agent hasn't encountered before.
