# Section 3: Pre-run Script Check (godotiq_server.gd)

## Overview

This section modifies the existing `_handle_run` function in `godotiq_server.gd` to perform a synchronous, lightweight script validation step before starting the game. If any scripts used by the target scene have compilation or parse errors, the handler returns a `SCRIPT_ERRORS` error via `_send_error` and does **not** start the game.

**File modified:** `/Users/salvatorefarruggio/Desktop/godotiq/godot-addon/addons/godotiq/godotiq_server.gd`

## Dependencies

- **Section 1 (Logger + Error Buffer):** Optional. The pre-run check does NOT use the Logger. It only uses `load() + can_instantiate()`.
- **Section 2 (check_errors Handler):** The autoload discovery helper can be reused. Script discovery for pre-run uses `PackedScene.get_state()` (no instantiation) rather than walking a live tree.

## Tests

This section is entirely GDScript. No Python unit tests. Python-side integration tests for SCRIPT_ERRORS handling are in Section 6.

### Manual Verification Stubs

```
# Manual: _handle_run saves all scenes before checking scripts
# Manual: _handle_run blocks play when scene scripts have errors
# Manual: _handle_run returns _send_error with code "SCRIPT_ERRORS"
# Manual: _handle_run proceeds normally when all scripts are clean
# Manual: Script discovery uses PackedScene.get_state() (no instantiation)
# Manual: Pre-run check includes autoload scripts
```

## Implementation Details

### Current _handle_run (Before This Section)

```gdscript
func _handle_run(peer_id: int, id: String, params: Dictionary) -> void:
    var scene = params.get("scene", null)
    if scene == null or scene == "main":
        EditorInterface.play_main_scene()
    elif scene == "current":
        EditorInterface.play_current_scene()
    else:
        EditorInterface.play_custom_scene(str(scene))
    _pending_run = {
        "peer_id": peer_id,
        "id": id,
        "timeout_at": Time.get_ticks_msec() + 5000,
    }
```

### Modified _handle_run

Insert three steps **before** calling any `play_*` method:

1. **Save all open scenes** — `EditorInterface.save_all_open_scenes()` ensures on-disk files match editor state.

2. **Collect script paths** based on `scene` parameter:
   - `"main"` or `null`: Load main scene from `ProjectSettings.get_setting("application/run/main_scene")` as `PackedScene`, use `get_state()` for scripts.
   - `"current"`: Use `EditorInterface.get_edited_scene_root()` and walk the live tree.
   - Specific `res://` path: Load as `PackedScene`, use `get_state()`.
   - In all cases, also collect autoload scripts from `ProjectSettings`.

3. **Check each script** — `load() + can_instantiate()` (synchronous, no `await`).

4. **Gate on results** — If errors found:
```gdscript
_send_error(
    peer_id, id, "SCRIPT_ERRORS",
    "Cannot start game: %d script(s) have errors. Use godotiq_check_errors for details." % errors.size()
)
return  # Do NOT call play_*() or set _pending_run
```

If no errors, proceed with original play logic unchanged.

### Helper: _get_scripts_from_packed_scene

```gdscript
func _get_scripts_from_packed_scene(scene_path: String) -> Array[String]:
```

1. `var packed = load(scene_path)` — if null, return empty array
2. Verify `packed is PackedScene` — if not, return empty array
3. `var state = packed.get_state()`
4. Iterate all nodes: `state.get_node_count()`
5. For each node, iterate properties: `state.get_node_property_count(node_idx)`
6. Check if property name is `"script"` — extract `resource_path` from value
7. Return unique script paths

Uses `SceneState` to read serialized data **without** calling `_init()`, `_ready()`, or `@onready` assignments.

### Helper: _get_autoload_script_paths

```gdscript
func _get_autoload_script_paths() -> Array[String]:
```

Same autoload discovery pattern as Section 2:
- Iterate `ProjectSettings.get_property_list()`, filter for `autoload/*`
- Strip `*` singleton marker with `.lstrip("*")`
- `.gd` files added directly, `.tscn` files use `get_state()` for root script

### Helper: _check_scripts_valid

```gdscript
func _check_scripts_valid(script_paths: Array[String]) -> Array[Dictionary]:
```

For each path:
1. `ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE)`
2. If null → `{file: path, error: "Failed to load script"}`
3. If `GDScript` and `not can_instantiate()` → `{file: path, error: "Script has compilation errors"}`
4. Otherwise → clean, skip

Synchronous. No `await`. For typical scenes (< 20 scripts + autoloads), blocking time is negligible.

### Error Format

Uses `_send_error` exclusively (not `send_response` with embedded error data). Consistent with existing error patterns. The Python bridge's `BridgeError` handling in `godotiq_run` (`__init__.py`) already catches all `BridgeError` exceptions. **No Python-side changes needed.**

### Why Not Use check_errors Handler

- `_handle_check_errors` is async (uses `await`)
- Uses Logger for detailed messages (optional, 4.5+)
- Pre-run check needs to be synchronous and fast — just pass/fail
- Pre-run is a gate; message directs agent to `godotiq_check_errors` for details

### Why PackedScene.get_state() Instead of Instantiation

- Instantiation runs `_init()`, `@onready`, `_ready()` — side effects
- Broken scripts cause instantiation to fail
- `SceneState` gives read-only access without executing GDScript
- For `"current"` scene, editor already has it instantiated — walk live tree

### Integration with _pending_run Polling

If check fails: `_pending_run` is never set → polling loop unaffected.
If check passes: original flow proceeds unchanged.

### Edge Cases

- **No scripts in scene**: Check passes trivially.
- **Main scene not set**: Skip PackedScene scan, only check autoloads.
- **Custom scene path invalid**: `load()` returns null → empty script list.
- **Scripts via preload/inheritance**: Not discovered by `SceneState`. Full coverage via `godotiq_check_errors(scope="project")`.

## Implementation Notes (Post-Review)

- "current" scene case reuses `_get_scene_script_paths()` instead of inline tree walk (deduplication)
- `_get_scene_script_paths()` now calls shared `_get_autoload_script_paths()` helper instead of inline autoload logic
- Used `trim_prefix("*")` instead of plan's `lstrip("*")` for correctness
- Used `save_all_scenes()` instead of plan's `save_all_open_scenes()` (correct API name)
