# Section 03: Godot Addon -- EngineDebugger Bridge and Game Runtime

## Overview

This section implements two GDScript files for editor-to-game IPC: the **EditorDebuggerPlugin** (runs in editor process) and the **game runtime autoload** (runs in game process). Together they enable the Python MCP server to request game-side operations (screenshots, performance snapshots) by relaying messages through Godot's EngineDebugger system.

### Architecture

```
Python MCP Server  <--WebSocket-->  godotiq_server.gd  <--EngineDebugger-->  Running Game
                                    godotiq_debugger.gd                      godotiq_runtime.gd
```

The WebSocket server (section 02) handles editor-side requests directly. For game-side requests (screenshot, perf_snapshot), the server forwards them to the debugger plugin, which relays to the running game via `EngineDebugger`. The game runtime executes the operation and sends results back through the same channel.

## Dependencies

- **section-01-godot-plugin**: Registers debugger plugin and runtime autoload, sets `_debugger.server = _server`
- **section-02-godot-server**: Provides `handle_game_response()`, `on_game_started()`, `on_game_stopped()`, `send_event()`

## Files to Create

| File | Purpose |
|------|---------|
| `godot-addon/addons/godotiq/godotiq_debugger.gd` | EditorDebuggerPlugin -- editor-side IPC bridge |
| `godot-addon/addons/godotiq/godotiq_runtime.gd` | Game autoload -- screenshot and perf snapshot execution |

## File 1: godotiq_debugger.gd

`@tool` script extending `EditorDebuggerPlugin`. Runs in editor process.

### Key Concepts

EngineDebugger IPC uses `"prefix:action"` message naming:
- `_has_capture(prefix)` receives only prefix (e.g., `"godotiq"`)
- `_capture(message, data, session_id)` receives full message (e.g., `"godotiq:screenshot_result"`)
- Must return `true` to indicate message was handled

### State

- `server` -- untyped var (avoids circular dependency). Set by `godotiq_plugin.gd`
- `_session_id: int = -1` -- active debugger session ID

### Methods

**`_has_capture(prefix: String) -> bool`**
- Returns `true` when `prefix == "godotiq"`

**`_capture(message: String, data: Array, session_id: int) -> bool`**
Routes game messages to server:
- `"godotiq:screenshot_result"` -> `server.handle_game_response("screenshot", data)`
- `"godotiq:perf_result"` -> `server.handle_game_response("perf", data)`
- `"godotiq:state_result"` -> `server.handle_game_response("state", data)`
- `"godotiq:input_result"` -> `server.handle_game_response("input", data)`
- `"godotiq:exec_result"` -> `server.handle_game_response("exec", data)`
- `"godotiq:nav_result"` -> `server.handle_game_response("nav", data)`
- `"godotiq:error"` -> broadcasts `runtime_error` event via server
- `"godotiq:watch_update"` -> broadcasts `watch_update` event via server
- Unknown -> returns `false`, pushes warning

**`_setup_session(session_id: int) -> void`**
- Stores `_session_id`
- Gets session via `get_session(session_id)`
- Connects `started` signal -> calls `server.on_game_started()`
- Connects `stopped` signal -> calls `server.on_game_stopped()`, resets `_session_id = -1`

**`send_to_game(message: String, data: Array) -> void`**
- Guards against `_session_id < 0`
- Calls `get_session(_session_id).send_message(message, data)`

### Skeleton

```gdscript
@tool
extends EditorDebuggerPlugin

var server  # untyped to avoid circular dependency
var _session_id: int = -1

func _has_capture(prefix: String) -> bool:
    return prefix == "godotiq"

func _capture(message: String, data: Array, session_id: int) -> bool:
    # Route messages to server.handle_game_response()
    ...

func _setup_session(session_id: int) -> void:
    # Store session, connect started/stopped signals
    ...

func send_to_game(message: String, data: Array) -> void:
    # Forward to game via get_session().send_message()
    ...
```

## File 2: godotiq_runtime.gd

Plain `Node` script (NO `@tool`). Runs as game autoload registered as `"GodotIQRuntime"`.

### Design

**`_ready()`**
- Guard: `if not EngineDebugger.is_active(): queue_free(); return`
- Register message capture: `EngineDebugger.register_message_capture("godotiq", _on_debugger_message)`

**`_on_debugger_message(message: String, data: Array) -> bool`**
- `"godotiq:screenshot"` -> `_take_screenshot(data[0] if data has dict else {})`
- `"godotiq:query_perf"` -> `_send_perf_snapshot()`
- Returns `true` if handled, `false` otherwise

**`_take_screenshot(params: Dictionary) -> void`**
1. `await get_tree().process_frame` (wait for viewport render)
2. `var img := get_viewport().get_texture().get_image()`
3. Guard: if viewport or image is null, send error result
4. Extract `scale` (float, default 0.5) and `format` (string, default "webp") from params
5. Resize if scale < 1.0: `img.resize(int(w * scale), int(h * scale))`
6. Encode to format:
   - `"png"` -> `img.save_png_to_buffer()`
   - `"jpg"` -> `img.save_jpg_to_buffer(0.85)`
   - default "webp" -> `img.save_webp_to_buffer()`
7. Convert to base64: `Marshalls.raw_to_base64(buffer)`
8. Send: `EngineDebugger.send_message("godotiq:screenshot_result", [b64, fmt, w, h])`

**`_send_perf_snapshot() -> void`**
Collects metrics:
- `fps`: `Engine.get_frames_per_second()`
- `draw_calls`: `RenderingServer.get_rendering_info(RENDERING_INFO_TOTAL_DRAW_CALLS_IN_FRAME)`
- `triangles`: `RenderingServer.get_rendering_info(RENDERING_INFO_TOTAL_PRIMITIVES_IN_FRAME)`
- `objects`: `RenderingServer.get_rendering_info(RENDERING_INFO_TOTAL_OBJECTS_IN_FRAME)`
- `texture_mem`: `RenderingServer.get_rendering_info(RENDERING_INFO_TEXTURE_MEM_USED)`
- `buffer_mem`: `RenderingServer.get_rendering_info(RENDERING_INFO_BUFFER_MEM_USED)`
- `video_mem`: `RenderingServer.get_rendering_info(RENDERING_INFO_VIDEO_MEM_USED)`
- `total_nodes`: `get_tree().get_node_count()`
- `orphan_nodes`: `Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT)`

Assembles dict, converts to JSON: `JSON.stringify(result)`
Sends: `EngineDebugger.send_message("godotiq:perf_result", [json_string])`

### Skeleton

```gdscript
extends Node

func _ready() -> void:
    if not EngineDebugger.is_active():
        queue_free()
        return
    EngineDebugger.register_message_capture("godotiq", _on_debugger_message)

func _on_debugger_message(message: String, data: Array) -> bool:
    match message:
        "godotiq:screenshot":
            _take_screenshot(data[0] if data.size() > 0 and data[0] is Dictionary else {})
            return true
        "godotiq:query_perf":
            _send_perf_snapshot()
            return true
    return false

func _take_screenshot(params: Dictionary) -> void:
    await get_tree().process_frame
    # Get viewport, resize, encode, send via EngineDebugger
    ...

func _send_perf_snapshot() -> void:
    # Collect metrics, JSON.stringify, send via EngineDebugger
    ...
```

## Important Notes

- **Frame await is mandatory** for screenshots -- ensures viewport has rendered
- **`_on_debugger_message` must return bool** -- EngineDebugger requires it
- **No `@tool` on runtime** -- runs in game process, not editor
- **EngineDebugger data arrays** must contain Variant-compatible types (String, int, float)
- **Guard with `EngineDebugger.is_active()`** -- prevents errors in exported builds

## Tests

No Python tests. Manual validation in Godot editor by running a scene and checking debugger message flow.

---

## Post-Implementation Notes

### Files Created/Modified
- `godot-addon/addons/godotiq/godotiq_debugger.gd` — replaced stub with full implementation (85 lines)
- `godot-addon/addons/godotiq/godotiq_runtime.gd` — new file (91 lines)

### Deviations from Plan

1. **`_capture` mapping keys**: Spec listed short names (`"screenshot"`, `"perf"`) for `handle_game_response()`. Implementation uses full EngineDebugger message names (`"godotiq:screenshot"`, `"godotiq:query_perf"`) to match what `godotiq_server.gd` actually stores in `_pending_game_requests[].method`. This is required for correct request matching.

2. **JSON string parsing in runtime**: Spec assumed `data[0]` would be a Dictionary in `_on_debugger_message`. Server actually sends `JSON.stringify(params)`, so runtime now parses the JSON string. Handles both String (from server) and Dictionary (direct call) inputs.

3. **Added null texture guard**: Spec said "guard if viewport or image is null" but didn't separate viewport vs texture. Implementation guards both `get_viewport()` and `get_texture()` independently.

4. **Scale clamping**: Added `clampf(scale, 0.1, 1.0)` — spec didn't mention bounds but 0/negative values would crash `resize()`.

5. **Post-resize dimensions**: Spec sent original `w, h` in screenshot result. Implementation sends actual post-resize dimensions for accurate metadata.

6. **Session signal cleanup**: Added old session signal disconnection in `_setup_session` to prevent stale callbacks on game restart. Not in spec.
