# Sprint 4b Research Findings

## 1. Codebase Patterns & Conventions

### Bridge Tool Pattern (Two-Layer Architecture)

**Layer 1: Implementation module** (`src/godotiq/tools/bridge/[tool].py`)
- Pure async function with business logic
- Imports `get_bridge()` from `godotiq.bridge.session`
- Makes bridge requests via `await bridge.request()` or `request_with_retry()`
- Returns plain dict, no error handling (lets exceptions bubble up)

**Layer 2: MCP tool wrapper** (`src/godotiq/tools/bridge/__init__.py`)
- `@mcp.tool()` decorated async function
- Calls the implementation function
- Wraps all exceptions: `BridgeConnectionError`, `BridgeTimeoutError`, `BridgeError`
- Returns error dict on exception, never raises
- Includes annotations: `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`

### Editor-side vs Game-side Tools

- **Editor-side** (no game needed): Use `bridge.request()` directly. Example: `editor_context`
- **Game-side** (game must run): Use `bridge.request_with_retry()`. Example: `screenshot`
- **Sprint 4b tools are ALL editor-side** — no game forwarding needed

### Server Handler Dispatch (GDScript)

All requests flow through `_dispatch()` match block in `godotiq_server.gd`:
```gdscript
func _dispatch(peer_id: int, id: String, method: String, params: Dictionary) -> void:
    match method:
        "ping": _handle_ping(peer_id, id)
        "editor_context": _handle_editor_context(peer_id, id)
        "run": _handle_run(peer_id, id, params)
        # ... Sprint 4b will add: scene_tree, node_ops, set_property, script_attach
```

Editor-side handlers call `send_response(peer_id, id, result_dict)` directly.
Error responses use `_send_error(peer_id, id, code, message)`.

### Tool Registration

In `src/godotiq/server.py`, tool modules are imported which triggers `@mcp.tool()` registration:
```python
from godotiq.tools import bridge  # imports __init__.py
```

### Protocol Constants

Timeouts in `src/godotiq/bridge/protocol.py`:
```python
TOOL_TIMEOUTS = {"ping": 2.0, "screenshot": 30.0, "perf_snapshot": 5.0, "editor_context": 5.0, "run": 10.0, "stop": 5.0}
```
Sprint 4b tools should add timeouts here (likely 5.0s for scene_tree/node_ops/set_property/script_attach).

### Test Patterns

**Bridge tool tests** (`tests/test_bridge/test_tools.py`):
1. Create `AsyncMock()` for bridge
2. Set return value for expected bridge method
3. `patch("godotiq.tools.bridge.[tool].get_bridge", return_value=mock_bridge)`
4. Call the tool function
5. Assert bridge was called with correct parameters
6. Assert return value is correct

---

## 2. Godot 4.6 EditorUndoRedoManager API

### Accessing the Manager

From `@tool` script in editor context:
```gdscript
var undo_redo = EditorInterface.get_undo_redo()  # Returns EditorUndoRedoManager
```

Note: `get_editor_interface()` is deprecated in 4.6; `EditorInterface` is a singleton.

### Action Lifecycle

```gdscript
undo_redo.create_action("Action name", merge_mode, custom_context, backward_undo_ops, mark_unsaved)
# ... add do/undo operations ...
undo_redo.commit_action(execute)  # execute=true runs "do" ops immediately
```

### Key Methods

| Method | Description |
|--------|-------------|
| `add_do_method(obj, method, ...)` | Register "do" method call |
| `add_do_property(obj, prop, value)` | Register "do" property change |
| `add_do_reference(obj)` | Prevent GC if "do" history is discarded |
| `add_undo_method(obj, method, ...)` | Register "undo" method call |
| `add_undo_property(obj, prop, value)` | Register "undo" property change |
| `add_undo_reference(obj)` | Prevent GC if "undo" history is discarded |

### Critical Patterns for Sprint 4b

**Adding a node:**
```gdscript
undo_redo.create_action("Add node")
undo_redo.add_do_method(parent, "add_child", node)
undo_redo.add_do_property(node, "owner", scene_root)  # CRITICAL for persistence
undo_redo.add_do_reference(node)
undo_redo.add_undo_method(parent, "remove_child", node)
undo_redo.commit_action()
```

**Removing a node:**
```gdscript
undo_redo.create_action("Remove node")
undo_redo.add_do_method(parent, "remove_child", node)
undo_redo.add_undo_method(parent, "add_child", node)
undo_redo.add_undo_reference(node)  # Prevent GC during undo history
undo_redo.commit_action()
```

**Reparenting a node:**
```gdscript
undo_redo.create_action("Reparent node")
undo_redo.add_do_method(old_parent, "remove_child", node)
undo_redo.add_do_method(new_parent, "add_child", node)
undo_redo.add_do_property(node, "owner", scene_root)
undo_redo.add_undo_method(new_parent, "remove_child", node)
undo_redo.add_undo_method(old_parent, "add_child", node)
undo_redo.add_undo_property(node, "owner", scene_root)
undo_redo.commit_action()
```

**Property change:**
```gdscript
undo_redo.create_action("Set property")
undo_redo.add_do_property(node, "name", new_value)
undo_redo.add_undo_property(node, "name", old_value)
undo_redo.commit_action()
```

### Critical: Owner Property

For nodes to appear in scene tree dock and persist on save, `owner` must be set to the edited scene root **after** `add_child()`. Without it, nodes are invisible and won't save.

### Operation Ordering

Group all "do" operations together and all "undo" operations together. Do NOT interleave.

### MergeMode

| Mode | Value | Description |
|------|-------|-------------|
| `MERGE_DISABLE` | 0 | Separate actions |
| `MERGE_ENDS` | 1 | Merges same-named within 800ms |
| `MERGE_ALL` | 2 | Full merge |

---

## 3. EditorInterface Scene Tree API

### Scene Tree Reading

| Method | Description |
|--------|-------------|
| `get_edited_scene_root()` | Root node of currently active scene tab |
| `get_open_scenes()` | File paths of all open scene tabs |
| `save_scene()` | Saves active scene, returns `OK` or `ERR_CANT_CREATE` |
| `mark_scene_as_unsaved()` | Flags scene tab as needing save |

**Limitations of `get_edited_scene_root()`:**
- Returns `null` if no scene is open
- Only returns the currently active scene tab
- May return `null` during editor startup

### Selection API

`EditorInterface.get_selection()` returns `EditorSelection`:

| Method | Description |
|--------|-------------|
| `get_selected_nodes()` | All selected nodes |
| `get_top_selected_nodes()` | Only top-level selected (excludes descendants) |
| `add_node(node)` | Add to selection (does NOT open in Inspector) |
| `remove_node(node)` | Remove from selection |
| `clear()` | Clear entire selection |

### Inspection

| Method | Description |
|--------|-------------|
| `edit_node(node)` | Selects node + shows in Inspector |
| `inspect_object(obj)` | Shows object in Inspector |

### Refreshing After Changes

Changes through EditorUndoRedoManager automatically trigger scene tree refreshes. For manual refresh:
- `node.notify_property_list_changed()` — refresh Inspector properties
- `EditorInterface.mark_scene_as_unsaved()` — flag unsaved (usually automatic with UndoRedo)

---

## 4. Testing Considerations

### Existing Test Setup

- Framework: pytest + pytest-asyncio (auto mode)
- Bridge tests use `AsyncMock` + `patch` for `get_bridge`
- Sprint 4b tools should follow the same pattern
- New test files go in `tests/test_bridge/` (e.g., `test_scene_tree.py`, `test_node_ops.py`)

### What to Test

- Default parameter handling
- Parameter validation and clamping
- Correct bridge request method and params
- Error response handling (BridgeConnectionError, BridgeTimeoutError, BridgeError)
- Return value structure

### Sources

- [EditorUndoRedoManager docs](https://docs.godotengine.org/en/stable/classes/class_editorundoredomanager.html)
- [EditorInterface docs](https://docs.godotengine.org/en/stable/classes/class_editorinterface.html)
- [EditorDebuggerPlugin docs](https://docs.godotengine.org/en/stable/classes/class_editordebuggerplugin.html)
- [EngineDebugger docs](https://docs.godotengine.org/en/stable/classes/class_enginedebugger.html)
- [PR #75694 - EditorInterface as singleton](https://github.com/godotengine/godot/pull/75694)
- [Owner property guide](https://duroxxigar.github.io/godot/Owner-Property/)
- [Modifying scenes through plugin](https://forum.godotengine.org/t/the-right-way-of-modifying-a-scene-through-plugin/23829)
